Storybook on-demand architecture
3x smaller builds & faster load times for built Storybooks
As the number of stories grows, it gets trickier to load them all in a performant way. That ends up bogging down the developer experience. We use Storybook to build Storybook so we also feel that pain.
In recent releases, performance has become a top priority. The latest versions include incremental yet noticeable improvements in build time and bundle size.
I am excited to share Storybook's new on-demand architecture, a fundamental change coming to 6.4 that improves performance for built Storybooks. We worked with the Webpack and Shopify UX engineering teams to cut bundle size by up to three times. Read on to see how.
Under the hood
Before we begin, letโs recap whatโs going on under the hood. Storybook is a collection of component examples called stories. These are small snippets of Javascript code that render a UI component (typically part of a design system or application) in isolation.
Stories are defined in a CSF file (Component Story Format). All stories related to a component are grouped together in the same file. Storybook's job is to take a story defined in the CSF file and render it in the browser, as requested by the user.
To render these examples, we need to load up all the associated code in the browser. For that, we use Webpack to create a JavaScript bundle that contains: all the CSF files, your components and resources required to render them. Plus Storybook's runtime.
Since bundle size has a massive impact on performance, we focused our efforts on slimming it down.
How to cut Storybookโs bundle size in half
When it comes to performance, Smaller = Faster. The smaller we can make the Storybook bundle, the faster it'll load for you. With that in mind, weโve made two architectural changes to speed up your developer experience:
- Code-splitting: enables faster load times for production Storybooks
- Smart file system caching: enables faster development startup
In previous versions of Storybook, all code was packaged into one big bundle. More components and stories resulted in heavier bundles which slowed down Storybook. It took awhile to boot up (especially when loaded over a network) or to start the development server.
In recent years, larger applications began to rely on bundle splitting. The idea is to split that large bundle into smaller, more manageable pieces. Additionally, tools like NextJS have pioneered techniques of lazy compilation. Building the entire application on startup takes time. Instead, they only build specific modules needed for a particular task that the user is focused on.
The key to bundle splitting is to only load the code required for the first render. Everything else is fetched asynchronously (via the import()
construct) as it's needed.
Applications have achieved this either by manually specifying and awaiting import()
s or by splitting automatically on page routes (as NextJS) does. The first option is more manual and gives you more control over the experience. The second option can be optimized at the framework level but usually restricts what you can do.
For Storybook, we worked with the Webpack team to explore both approaches.
What didnโt work: Manual import()
functions
Since Storybook 6.1, itโs been possible to use import()
functions to code split your Storybookโ using the experimental feature: loaders
.
// A CSF file that establishes a import "boundary" to the component file
export default {
title: "MyComponent",
loaders: [async () => ({ Component: await import('./MyComponent') })],
// In CSFv3 you could define this render() function for all components
render: (args, { loaded: { Component } }) => <Component {...args} />,
};
export const MyStory = {
args: { arg1: 'value' }
};
In the above CSF file, there is no direct, static import
of ./MyComponent ;
simply an async import()
inside the supplied loader.
With this setup, all the CSF files create a single (initial) bundle. Whereas each component file will form its own bundle, along with its dependencies.
In prototyping this approach, we discovered two major downsides:
- The requirement for the user to write a loader for every component is unwieldy, unintuitive and makes it harder to reuse stories. Code splitting seems like an optimization detail that you, a user of Storybook, shouldn't need to care about.
- In experiments, we often found that the initial bundle that includes all CSF files is a large fraction of the total size of Storybook, reducing the benefits of code splitting.
That large initial bundle was often because it's hard to keep CSF "pure" of other component dependencies. Also, minimizing the initial bundle size is particularly important in cases where Storybook is embedded or composed into other contexts.
What worked: Automatic Code Splitting
The alternate approach is for Storybook's store to treat each CSF file as a separate asynchronous import()
and load the stories "on-demand":
In this way, each CSF generates its own bundleโthe component plus the minimum dependencies required to load and render the story. No code changes are necessary. This all happens behind the scenes with no user intervention.
This approach is more complex and leads to caveats on where and when you can use it (see below). But generally works for even the most complex Storybooks with minimum changes.
This behaviour will be the default in Storybook 7.0 โ itโs available in 6.4 via the storyStoreV7
feature flag (see below); the previous single-bundle behaviour is still enabled by default in 6.4 installs.
Performance wins in 6.4
The primary purpose of introducing code splitting is to improve the performance of your Storybook. That is, the time it takes to install and startup and how long it takes to download and interact with a built Storybook.
The changes in 6.4 are focused on enabling code splitting in Storybook. The immediate impact will be smaller bundle sizes, which means that built Storybooks should load up faster.
As an example, the Chromatic Storybook, a larger Storybook with 2000 stories, displayed the following behaviour when upgraded to the v7 store:
Similarly, Shopify's Storybook saw a 67.5% saving in initial bundle size after enabling the v7 store.
Whatโs next for performance in 6.5+
Code splitting alone doesnโt necessarily improve the developer experience of working with Storybook. Generating multiple code-split bundles can even take longer than creating one large bundle. It depends on the duplication of code across bundles and the complexity of optimizing the contents.
However, it unlocks further optimizations. A key one is the use of lazy compilation to only generate the bundles required to render the stories currently visible on the screen. Lazy compilation is an experimental Webpack 5 feature, conceptually similar to NextJS's just-in-time page building.
Experiments with lazy compilation and file system caching have demonstrated that it should be possible to reduce development start time and rebuild times by a factor of 3-5x on large projects. This will be a major focus of Storybook 6.5.
Additionally, other optimizations to the Webpackโs splitting mechanism are now unlocked. We encourage users to try tweaking Storybook's default Webpack settings and contributing improvements back to the 6.5 release.
Caveats
The tricky part of the automatic code-splitting approach is that we no longer load all the CSF files at "bootup" time. Instead, we need to calculate the Storybook's list of stories (the "Story Index") statically from a node context. This means we don't evaluate your story files but simply parse them and analyze the resultant AST. This has some limitations on what you can do in your CSF files (some of which we may remove in future iterations):
- Only the CSF format (v1-v3) is supported;
storiesOf()
is not. - CSF titles and story names must be statically defined (i.e.
title: 'Component'
, nottitle: MyTitle
). - Custom
storySort
functions are provided with a more limited API.
Try it today
Automatic code-splitting is now available in the 6.4 beta. It takes just a minute to try it out, you can run the following command at the root of your project:
npx sb upgrade --prerelease
If youโre not using Storybook already, it's easy to get started:
npx sb@next init
Then enable the feature flag
// .storybook/main.js
module.exports = {
features: {
storyStoreV7: true,
}
};
Help shape the next-generation of Storybook!
Storybook on-demand architecture brings significant performance benefits and allows you to experiment with other Webpack optimizations.
Developers use Storybook daily to build hundreds of components and thousands of stories. What tweaks have you made to speed up your Storybook? We'd love to hear from you. Reach out on Twitter or drop by the Storybook Discord.
The on-demand architecture feature was developed by Tom Coleman (me!), Juho Vepsรคlรคinen and Michael Shilman with feedback from the entire Storybook community.
Storybook is the product of over 1320 community committers and is organized by a steering committee of top maintainers. You, too, can contribute a new feature, fix a bug, or improve the docs. Join us on ย Discord, support us on Open Collective, or just jump in on GitHub.