feat(storybook): add on-demand performance metrics scripts - #5229
feat(storybook): add on-demand performance metrics scripts#5229sauldom102 wants to merge 2 commits into
Conversation
Expose the Storybook performance panel's numbers as JSON, so agents and humans can check a component while building or changing it: pnpm perf-metrics F0Button --snapshot perf-changed.ts measures the stories a PR adds or changes and reduces them to the few facts worth attention, for a future PR comment. Highlight thresholds are set from the measured distribution across the library, not by feel: 102 of 102 snapshot stories record at least one render cascade, so "any cascade" would highlight nothing. Layout shift is highlighted by CLS score rather than shift count because the count is not reproducible between runs, while the score is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
🔍 Review policy: FeatureThe PR title starts with Required approvals
How this was decided
Policy source: |
✅ No New Circular DependenciesNo new circular dependencies detected. Current count: 0 |
📦 Alpha Package Version PublishedUse Use |
🔍 Visual review for your branch is published 🔍Here are the links to: |
✅ No breaking public API changesNo public exports were removed, renamed, or had existing props/types changed in a breaking way compared to Comparing
|
♿ Accessibility (axe) — components changed in this PR✅ No a11y issues in the stories this PR changed. Scope: only stories in the files/component folders this PR changed. It can't yet flag downstream ripple from shared-code/token changes, or diff against |
Coverage Report for packages/react
File CoverageNo changed files found. |
perf-changed.ts keyed off changed *.stories.tsx files, so a PR that changed a component without touching its story measured nothing at all — the common shape of a fix. It now diffs every source file under packages/react/src and maps each to the stories that render it, walking up to the owning component directory so nested files attribute correctly. The walk stops at src/<zone>/<Name> depth so a shared utility cannot be attributed to half the library, and test, snapshot and docs files are excluded outright: fix(F0Chat) (bf41fa6) changed two components and two __tests__/ files, and the test files alone attributed 38 stories. Each measured story records `measuredBecause` ("story" or "source") so the comment can attribute a finding to the component rather than imply the author edited a story they never opened. Adds --head so a range other than "…...HEAD" can be previewed locally. Verified against real history: the component-only commit reports 39 stories affected where it previously reported none, and a docs-only commit still reports nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
✅ No untranslated copy addedEvery user-visible string in this PR comes from the i18n layer. Codebase total unchanged at 133. |
✅ Storybook docs — no pages lostEvery page reachable on Links point at this PR's Storybook build — browse the full Storybook. Snapshot of the Storybook index (docs pages + stories) compared against |
Description
Follow-up to #5223, which added the Storybook performance panel. That panel only shows its numbers to someone with Storybook open; this exposes the same measurements as JSON on demand, so an agent or a developer can check a component while building or changing it. Nothing here runs in CI or gates anything.
Not included
The workflow wiring.
agentic-checks.yamlruns agents but only writes to the job summary, while the PR-comment action lives instorybook-tests.yaml; joining the two is the remaining piece. The prompt is here so that change is small when it lands.Notes for reviewers
Why the output is split into
deterministicandtimingMeasuring the same story five times on an idle machine:
totalBlockingTimeranged 42–47ms) and is contaminated by the rest of the page:longTasksin Storybook is frequently axe-core, not the component. Reported, but never used to raise a highlight, and the agent prompt tells the model not to call anything "slow" based on it.The split is also why collection is cheap: the React counts are complete as soon as the story mounts, because the Profiler records renders as they happen regardless of when collection starts.
Highlight thresholds come from the measured distribution, not from intuition
The obvious rule — "flag any render cascade" — turns out to be useless here. 102 of 102 snapshot stories record at least one cascade (median 3), because Storybook's own decorators and providers render around the story. A rule that fires on 100% of stories highlights nothing; the first version of this flagged 40 out of 40 measured stories.
Thresholds are now set at roughly p95 of the measured distribution across those 102 stories:
forcedReflows > 0andslowUpdates > 0stay absolute — their measured base rates are 0/40 and 2/40. On a real changed-set this now highlights 4 stories out of 40.Layout shift is highlighted by CLS score, never by shift count
Shift count is not reproducible. The same five F0Card stories measured three times gave three different sets:
card--with-childrencard--compactcard--with-actions-and-linkA shift is only recorded when the browser happens to paint between the two layouts. Highlighting on count would mean the same PR gets a different comment on every re-run.
The score is stable once it clears the noise floor — across three runs
ApplicationFramescored 0.1943 / 0.1932 / 0.1936 andAnalyticsDashboardscored 0.0472 all three times, while every story that flickered scored ~0.0001. So the rule iscls >= 0.01, an order of magnitude above that floor and below Core Web Vitals' 0.1 "needs improvement" line. Across the library it selects 4 of 102 snapshot stories:patterns-app-shell-applicationframe--snapshotpatterns-analyticsdashboard--snapshotpatterns-resource-header--snapshotpatterns-navigation-sidebar-chatlist--snapshotApplicationFrame is worth a look independently of this PR — 0.198 is approaching the 0.25 that Core Web Vitals calls "poor".
Two traps worth keeping in mind when editing these scripts
note()helper that writes to stderr, becauseconsola.info/consola.successwrite to stdout — routing progress through them corrupts the payload and| jqfails with "Invalid numeric literal".perf-changed.tsanchors its globs with:(top); without it, running frompackages/react(wherepnpm --filterputs you) matches nothing and the script cheerfully reports "no story files changed" on a PR that changed plenty.Update: component changes are now measured
Detection keyed off changed
*.stories.tsxfiles, so a PR that changed a component without touching its story measured nothing — the common shape of a fix. It now diffs every source file underpackages/react/srcand maps each to the stories that render it.How a source file finds its stories
The file's directory is walked upwards until one is found with stories beneath it, so nested layouts attribute correctly (
F0Button/internal/helpers.ts→F0Button, whose stories may sit at the root or under__stories__/).The walk stops at
src/<zone>/<Name>depth. Without that floor,src/lib/utils.tswould walk up tosrc/liband drag in every story in the tree.Story files keep their exact-path matching, and are processed first so a precise attribution is never overwritten by the vaguer directory one. Results are deduplicated by story id, since a PR that changes both a component and its story reaches the same stories twice.
Test and docs files are excluded — this one bit
fix(F0Chat)(bf41fa6e4) changed two components and two__tests__/files. The test files alone attributed 38 stories, so a PR that only adjusted assertions would have triggered a full performance comment about code whose behaviour never moved.__tests__/,__snapshots__/,*.test.*,*.spec.*,*.mdand*.mdxare now filtered out. Verified: that commit drops from 4 changed files to the 2 real component files and still reports; a docs-only commit reports nothing.New fields and flag
measuredBecause—"story"if the story's own file changed,"source"if only its component did. The agent prompt uses it to attribute findings to the component rather than implying the author edited a story they never opened.changedFilereplacesstoryFile, since the file that pulled a story in is often not a story file.storiesFromStoryChanges/storiesFromSourceChangesin the summary.--head <ref>so a range other than…...HEADcan be previewed locally — "what would this commit have reported?".A real finding from testing this
Measuring
F0AiChatTextArea(pulled in by the F0Chat commit) shows it well outside the library norm:updatesp50 of 21 against a library p50 of 6, 18 forced reflows where the measured base rate elsewhere was 0 of 40, and CLS up to 0.72 — nearly 3× the 0.25 that Core Web Vitals calls "poor".Not addressed here; noting it because it is the kind of thing this tooling exists to surface.