perf(storybook): scope the renderer matrix to slot-owning stories - #308
Merged
Merged
Conversation
`test-storybook` ran every indexed story against all four renderer previews (built-in, MUI, PrimeReact 11, PrimeReact 10) in both appearances with axe, even though only components that participate in the renderer slot system can render differently between them. A story whose component never resolves a renderer slot renders identical DOM on every renderer, so three of its four runs prove nothing beyond what the built-in run already proved; they only add axe time linearly with every story added. Scope the renderer dimension to what it can actually distinguish. A story now runs the full four-renderer matrix only if its component owns a renderer slot (calls `unstable_useSlot`, directly or through a locally declared Core fallback) or transitively imports a component that does - so a composite like `CommandDialog` or `DataPage`, which renders slotted primitives inside it, still runs the full matrix. All other stories run once, on the built-in renderer, in both appearances, still with axe - nothing loses its axe coverage. The partition is derived, not hand-maintained. The slot registry comes from a source scan for `unstable_useSlot(` call sites - the same mechanism `RendererContext` resolves against - so a newly slotted component is picked up the moment it starts using it. Each story's component is read from the already-built Storybook index (`componentPath`, falling back to the story module itself when a story has no declared `component`), and reachability from that component to a slot-owning module is computed by following the local import graph (`Storybook/scripts/lib/renderer-matrix-scope.mjs`). Type-only imports are followed too, so this can only over-include a story, never drop one that genuinely composes a slotted primitive. `Storybook/preview/main.ts` now accepts an optional `CRATIS_STORYBOOK_MATRIX_STORY_GLOBS` override for its `stories` field, which `run-story-matrix.mjs` sets to the derived matrix subset for the non-built-in adapter runs (the built-in run always sees every story). Storybook's own `stories` config is used rather than Vitest's `test.include`, because the Storybook Vitest plugin overwrites `test.include` unconditionally. Full automatic derivation was achievable here, so there is no hand-maintained mapping to go stale. As a safeguard against the derivation itself silently regressing (for example the `unstable_useSlot` scan matching nothing after a rename, which would quietly send every story down the built-in-only path), `verify-storybook-indexes.mjs` now pins the derived slot-owning module count (14) and matrix story count (175) alongside the existing 326/74 story/docs pins, and a new spec (`Storybook/for_renderer_matrix_scope`) exercises the derivation against known built-in-only components (ComboBox, ToggleGroup, Tabs, TagGroup, Breadcrumbs), known composites (CommandDialog, DataPage, ColumnFilterMenu, SchemaEditor), and a regression fixture for a defect caught while writing this: an earlier revision's traversal broke as soon as it found a slot-owning module and then wrongly memoized every sibling visited so far as matrix-worthy, regardless of whether that sibling itself reached a slot. Measured locally (`yarn build && yarn verify-indexes && yarn test-storybook`, this checkout): before, 4 previews x 326 stories x 2 appearances = 2608 cases in 83s; after, 1 built-in preview x 326 stories + 3 renderer-distinguishing previews x 175 matrix stories, x 2 appearances = 1702 cases in 60s. No-release: this only changes what CI exercises, not consumer-facing behavior.
This was referenced Sep 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
test-storybookran every indexed story against all four renderer previews (built-in, MUI, PrimeReact 11, PrimeReact 10) in both appearances with axe, even though only components that participate in the renderer slot system can render differently between them. This scopes the renderer dimension to what it can actually distinguish: a story runs the full four-renderer matrix only if its component owns a renderer slot, or transitively composes one; everything else runs once, on the built-in renderer, in both appearances, still with axe.Measured locally (
yarn build && yarn verify-indexes && yarn test-storybook, same checkout, before/after this change):No consumer-visible change —
no-release.Changed
test-storybooknow runs the full built-in/MUI/PrimeReact 11/PrimeReact 10 matrix only for the 175 of 326 stories whose component owns or composes a renderer slot; the remaining 151 stories run once, on the built-in renderer, in both appearances, still with axe (The story matrix runs four renderers over components that only one renderer can render #302)verify-storybook-indexes.mjsnow pins the derived slot-owning module count (14) and matrix story count (175) alongside the existing 326 story / 74 docs pins (The story matrix runs four renderers over components that only one renderer can render #302)Added
Storybook/scripts/lib/renderer-matrix-scope.mjsderives the renderer-matrix partition automatically: the slot registry comes from a source scan forunstable_useSlot(call sites (the same mechanismRendererContextresolves against), each story's component is read from the already-built Storybook index, and reachability to a slot-owning module follows the local import graph — so a composite likeCommandDialogorDataPagethat renders a slotted primitive inside it stays in the matrix automatically, and a newly slotted component joins it the moment it starts usingunstable_useSlot(The story matrix runs four renderers over components that only one renderer can render #302)Storybook/for_renderer_matrix_scopespec covering known built-in-only components (ComboBox, ToggleGroup, Tabs, TagGroup, Breadcrumbs), known composites (CommandDialog, DataPage, ColumnFilterMenu, SchemaEditor), and a regression fixture for a traversal defect caught while writing this derivation (The story matrix runs four renderers over components that only one renderer can render #302)Notes for reviewers
unstable_useSlotscan started matching nothing after a rename, which would quietly send every story down the built-in-only path),verify-storybook-indexes.mjspins the derived counts and fails loudly if they drift, and the new spec exercises known matrix/built-in-only cases directly.Storybook/preview/main.ts'sstoriesfield now accepts an optionalCRATIS_STORYBOOK_MATRIX_STORY_GLOBSenv override, set byrun-story-matrix.mjsfor the non-built-in adapter runs only. This goes through Storybook's ownstoriesconfig rather than Vitest'stest.include, because the Storybook Vitest plugin unconditionally overwritestest.include.cd Source && yarn cistill passes unchanged.