Last updated: May 09, 2026
Storybook’s “on-demand compilation” actually names two distinct engineering layers that the marketing collapses into one phrase. The first is on-demand architecture: a static CSF indexer that parses each story file’s AST in Node and exposes a story index the sidebar reads before Webpack starts. The second is lazy compilation: a Webpack-only build-time feature that wraps each per-CSF chunk in a proxy module and compiles it on first visit. Pulling the layers apart explains why CSF boots fast even without lazy compilation.
- Static CSF indexing shipped as
storyStoreV7in Storybook 6.4 (December 2021), became default in 7.0, and the legacy V6 path was removed in 8.0. - Webpack lazy compilation shipped in Storybook 6.5 (May 2022) and remains an experimental Webpack feature, opt-in via
core.builder.options.lazyCompilation. - The static indexer requires literal
titlestrings and statically named exports;storiesOfand computed titles fail at index time, before Webpack runs. - Vite-builder Storybook skips the proxy layer because native ESM already loads each CSF on demand.
- Lazy compilation trades cold-boot time for a perceptible pause on every newly visited story — a real DX cost on design-system browsing patterns.
Two features, one label
The query “storybook on demand compilation” gets searched by people trying to do two different things, and the SERP cannot tell them apart because the two top-ranking Storybook posts run them together. On-demand architecture changes how the story index is built and how the runtime loads stories: the index is computed statically in Node, the sidebar renders before any user code is bundled, and every CSF file becomes its own dynamic-import chunk. Lazy compilation changes when Webpack actually emits each of those chunks: instead of producing the full dev bundle on boot, Webpack injects proxy modules and only compiles the real code when the browser asks for it.
The difference matters because the levers behave differently. On-demand architecture is on by default in any modern Storybook and explains the bulk of the perceived “fast boot” experience even on a Webpack project. Lazy compilation is an extra opt-in that bites only when story count is large enough that compiling all CSF files at boot still hurts. Conflating the two sends people to flip a flag that does very little for their project size, and away from the architectural reason the sidebar appears so quickly.
I wrote about component-driven workflow if you want to dig deeper.

The diagram shows the boot sequence side by side: the indexer pass runs entirely in Node and produces routes plus a sidebar tree before the dev server hands control to the browser. Lazy compilation only enters once the user clicks a story and the browser issues its first dynamic-import request — it sits downstream of indexing, not as a replacement for it.
The static CSF indexer is why CSF boots faster than Webpack
The fast boot trick has nothing to do with Webpack. When you run storybook dev, the manager process globs the configured stories entries, AST-parses each CSF file with the official indexer, and exposes the generated story index over the dev server at /index.json. That document is the entire sidebar — every story id, title, name, and import path — and it ships to the browser before any user code is compiled. The browser can render the navigable tree, generate every route, and even highlight the active story while Webpack is still spinning up.
// Excerpt from the generated story index served at /index.json
{
"v": 5,
"entries": {
"components-button--primary": {
"type": "story",
"id": "components-button--primary",
"name": "Primary",
"title": "Components/Button",
"importPath": "./src/components/Button.stories.tsx",
"tags": ["autodocs", "story"]
},
"components-button--secondary": {
"type": "story",
"id": "components-button--secondary",
"name": "Secondary",
"title": "Components/Button",
"importPath": "./src/components/Button.stories.tsx"
}
}
}
Notice what is and is not in that document. There is enough metadata to build the entire UI shell — id, title, name, tags, importPath — and zero compiled JavaScript. The browser uses the index to construct the URL space and the tree before Webpack has finished compiling any chunk. That is the real on-demand mechanism: the sidebar is decoupled from the build, and the build is decoupled from sidebar order. Add or rename a story and the indexer republishes the index while the sidebar updates without rebuilding any user code.
Related: Storybook’s ESM shift.

Here’s what the example produces.
The captured terminal trace confirms the sequence. The indexer ready message lands well before webpack compiled successfully, and the manager UI is interactive in between. This is the part of the boot that gets called “instant” — it is not Webpack getting faster, it is the sidebar being emitted by a separate Node-only pass that runs ahead of compilation.
Storybook 7 rewrote this pipeline behind the public experimental_indexers API, which lets community packages teach Storybook how to derive stories from non-CSF sources — Svelte SFCs, MDX, Vue templates — by returning a createIndex(fileName) result that conforms to the same JSON shape. The contract is documented in the official indexers reference, and the RFC discussion #23176 records why the older storyIndexers preset was replaced.
What the indexer contract forbids — and what the error looks like when you break it
The indexer is a static AST analysis, not a runtime evaluation. That single fact dictates everything you can and cannot do in a CSF file. Anything the indexer cannot resolve by reading the source — without executing it — is a contract violation that fails before Webpack runs.
The contract’s load-bearing constraints are concrete:
- The default export must include a
titlethat is either a string literal or omitted entirely. Computed titles are unsupported. - Story exports must be named identifiers —
export const Primary = .... Dynamic names likeexport const [storyName] = ...cannot be indexed. - Stories must live in the file the index points at; re-exporting a story object from another module produces an entry the runtime cannot resolve.
- The legacy
storiesOf()imperative API is no longer indexable in Storybook 8 and above.
// This default export breaks the indexer
export default {
title: `Components/${process.env.PRIMARY_BRAND}/Button`, // computed at runtime
component: Button,
};
// stderr from `storybook dev`:
// Unable to index ./src/components/Button.stories.tsx
// CSF: unexpected dynamic title
Each violation surfaces a different stderr line. A computed title trips the AST validator and yields the message above, logged against the offending file. storiesOf usage in a Storybook 8+ project surfaces a hard error pointing readers at the migration RFC: the project’s deprecate storiesOf RFC is the canonical document, and the deprecation PR #23938 records the removal path. The fix is always the same shape: convert the dynamic CSF construct into a static one, or write a custom indexer that produces the equivalent JSON entries from a non-CSF source.

The screenshot of the indexer reference page doubles as a contract spec, but it pays to read it the way the docs do. IndexInput has a small required core — primarily type and the export identity the runtime needs to load — while importPath is optional and defaults to the file the indexer was handed, and most of the rest (title, name, tags, metaId, the optional __id) are either optional or filled in from sensible defaults when the indexer omits them. A custom indexer only has to produce the required core; everything else is there for the cases where you need to override what the default flow would already compute, not as a per-row shopping list to fill in for every story.
How Webpack’s lazy compilation actually fires on first visit
Lazy compilation is the second-stage feature, and it is a Webpack experiment, not a Storybook invention. Storybook’s role is to wire it through with the right options:
// .storybook/main.js
export default {
framework: '@storybook/react-webpack5',
core: {
builder: {
name: '@storybook/builder-webpack5',
options: {
lazyCompilation: true,
fsCache: true,
},
},
},
};
The mechanism, as documented on the Storybook lazy compilation post, is straightforward once you watch a network panel during a cold boot. Webpack emits a placeholder proxy module for every CSF chunk. When the browser dynamically imports a story, the proxy fetches the real chunk from a dev-server endpoint Webpack registered for lazy compilation requests. The dev server invalidates the proxy in its compiler instance, runs only the dependency graph rooted at that CSF file, emits the chunk, and serves it. Subsequent visits hit the warm chunk; subsequent edits invalidate only the affected CSF and its dependents.
Rolldown’s bundling story goes into the specifics of this.

The architecture diagram traces the lifecycle: indexer pass publishes the story index, manager renders the sidebar, browser dynamic-imports a CSF chunk, the proxy module hits the dev-server middleware, and only then does Webpack compile that subgraph. The split between “indexer pass” and “Webpack pass” is the line the SERP refuses to draw — and it is also the line that explains why the sidebar feels instantaneous even before lazy compilation is enabled.
The first-visit latency tax nobody quotes
Cold-boot time is one number. First-visit latency is a different number, and lazy compilation makes the second one worse on purpose. Each new story click triggers a fresh compile of that file’s dependency graph, and on a real component the cost is rarely zero.
Public claims about “how fast” each configuration is should be read with a wide error bar. Storybook’s own lazy-compilation announcement describes large boot improvements — the figure most often cited from the team’s posts is “up to 3x” cold-boot reduction, which is meaningfully sized but a long way short of an order of magnitude — and the team has also published several boot-time comparisons against the Vite builder over the 7 → 9 line. The qualitative picture from those posts is more conditional than the loud headlines suggest: enabling lazy compilation visibly cuts cold boot on Webpack projects with hundreds of stories; the Vite builder generally wins HMR and warm-rebuild and often wins cold startup as well, though Storybook’s own published Webpack-to-Vite benchmark does not put Vite ahead in every cold/warm case across every fixture; warm-cache rebuilds are uniformly small numbers regardless of builder.
What none of those posts include is a reproducible benchmark with raw logs, locked package versions, and pinned hardware. That is also why this article does not publish its own number table: with no shared fixture repo, no raw timing dataset, and no way to fix the dependency graph against a public commit, any “p50/p95” claim is just a screenshot. If you need real numbers for a budget conversation, the only honest path is to run storybook dev against your own repo with telemetry off and time the four states yourself: cold boot, first uncompiled-story click, warm rebuild on edit, and rebuild on a story that imports a heavy dependency. The same measurement on the same machine across two builder configurations is more decision-useful than any third-party table.
The qualitative shape, which is borne out across most published comparisons, is what matters for choosing knobs. Lazy compilation reshapes the cost from “one big bill at boot” to “many small bills on every newly visited story.” Cold boot drops noticeably. First-visit time on an uncompiled story grows from “already there” to “a beat between click and paint.” Rebuilds get faster because the graph is smaller. The Vite builder is the most consistent configuration for winning on both ends of the curve at once, even if no single public benchmark puts it on top of every column.

The dashboard view of the same fixture makes the trade-off legible even without numbers. Lazy compilation is not free latency; it reshapes the latency curve. You pay less on cold boot and more on every cold story click — fine for a developer camped on one component, miserable for a designer browsing 200 stories during a design-system review.
Why Vite-builder Storybook skips this whole machinery
The Vite builder gets the boot win without any Webpack proxy because native ESM in the browser already serves modules on demand. Vite’s dev server compiles a CSF file when the browser requests it via an import() call; there is no equivalent of the proxy module because there is no Webpack to wrap. The Storybook indexer still runs the same way it does on the Webpack builder — that pass is builder-agnostic — but the second-stage lazy work is built into how Vite serves modules in dev.
That changes the recommendation. On a greenfield project, Vite-builder Storybook hands you the architecture cleanly: per-CSF laziness by default, no experimental flag, no Webpack-specific dev-mode quirks to reason about. The Webpack builder docs still document lazyCompilation as experimental and as a development-mode feature, and that label has not changed across the 7 → 8 → 9 → 10 line. If your team is not locked into Webpack-specific tooling, switching builders deletes the entire problem class.
Related: Vite’s module graph internals.
A decision rubric for which knob actually moves your boot time
The right knob depends on your story count and your builder lock-in. Lazy compilation is a development-mode Webpack feature — Storybook’s Webpack configuration docs describe it that way, and it does not flow into the static output that storybook build produces. Whatever shape the dev graph takes when lazy compilation is on, the production graph is built by a separate run that does not use it. The discipline that actually protects production parity is to run storybook build in CI and assert against the produced graph there, not to read the dev-mode shape and assume it predicts the static export.
| Project shape | Recommended config | Stops being right when… |
|---|---|---|
| < 100 stories, Webpack-locked | Webpack default — leave lazyCompilation off | Cold boot crosses the patience threshold; the on-demand architecture alone is enough at this scale |
| 100–500 stories, Webpack-locked | Webpack + lazyCompilation + fsCache | Designers complain about the click pause more than developers complain about boot |
| 500+ stories, Webpack-locked | Webpack + lazyCompilation + fsCache + SWC swap | You can move to Vite — the SWC swap is a half-measure compared to dropping Webpack |
| Any size, builder choice open | Vite builder | You depend on a Webpack-only loader the Vite builder cannot replace |
| Static export to CDN with chunking SLA | Whichever dev config wins your inner loop; verify the production graph by running storybook build in CI and asserting on the actual chunks it emits |
CI no longer catches the chunk-shape regression you actually care about — at which point the missing assertion is the bug, not the dev-mode flag |
The SWC compiler swap is an orthogonal lever. It replaces Babel inside the existing Webpack pipeline and helps both default and lazy configurations, but it does not change the on-demand architecture story. If lazy compilation is off the table for any reason, swapping in SWC is a safe way to take a meaningful slice off cold-boot time without touching the dev/prod split at all.
For more on this, see why teams pick Vite.
Version reality check, Storybook 7 → 8 → 9 → 10
Most of the SERP for this query is still circa 2021–2022. Several things have actually moved since then. storyStoreV7 became the default in 7.0, and the V6 path — including storiesOf() — was removed in 8.0; the migration guide from 6.x to 8.0 records the breaking change. The indexer API was rewritten in 7.0 from the older storyIndexers preset to the new experimental_indexers contract; PR #23938 is the deprecation, and the new contract is the one documented at main-config-indexers. The 9.x line tightened the framework packages and dropped a number of legacy adapters, and the 10.x line — with 10.3.x the current release line at the time of writing — continues that direction while leaving the indexer contract and the Webpack lazyCompilation flag conceptually unchanged.
On the builder side, the picture has also evolved past the early Vite 5 references that show up in older posts. Vite has continued shipping major releases, and the @storybook/builder-vite package tracks Vite’s current line; the Storybook docs are the authoritative source for which Vite range a given Storybook version supports, because that pairing is the one that actually gets exercised in CI. The Webpack lazyCompilation flag still has not graduated out of experimental status in either Webpack itself or Storybook’s wrapper, which is why every reference to it in current Storybook docs still calls it experimental and dev-only.
Background on this in recent Storybook releases.
Anyone reading the 6.4 announcement post or the 6.5 lazy-compilation post as a current reference is reading something that has been technically correct in spirit and stale in detail for several major versions. The canonical MIGRATION.md on the storybookjs/storybook repository is the live primary source for what has actually changed, and it is the right document to consult before debating which flags to flip.
The takeaway is small but practical. Most of the boot win attributed to “on-demand compilation” is the static CSF indexer running ahead of Webpack — that is on by default, you do not need a flag, and it is what makes the sidebar feel instant. Lazy compilation is the right secondary lever only on Webpack-locked projects with hundreds of stories, and only if you can stomach the first-visit click pause; because it is a dev-mode feature, it is not the thing to worry about for production chunking, which is what storybook build in CI is for. If your project is not Webpack-locked, switching to the Vite builder retires most of the question entirely.
References
- Storybook on-demand architecture announcement (storybook.js.org)
- Storybook lazy compilation for Webpack (storybook.js.org)
- Storybook performance: from Webpack to Vite (storybook.js.org)
- Indexer API reference (storybook.js.org/docs)
- Storybook Webpack configuration reference (storybook.js.org/docs)
- RFC: Indexer API discussion #23176 (github.com/storybookjs)
- PR #23938 — Deprecate storyStoreV6 and storyIndexers (github.com/storybookjs)
- Migration guide from Storybook 6.x to 8.0 (storybook.js.org/docs)
- Storybook 10.3 release notes (storybook.js.org)
- Webpack experiments configuration (webpack.js.org)
- storybookjs/storybook MIGRATION.md (github.com)










