Back to blog

Storybook on-demand architecture

3x smaller builds & faster load times for built Storybooks

loading
Tom Coleman
β€” @tmeasday

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:

  1. 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.
  2. 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', not title: 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.

Join the Storybook mailing list

Get the latest news, updates and releases

6,543 developers and counting

We’re hiring!

Join the team behind Storybook and Chromatic. Build tools that are used in production by 100s of thousands of developers. Remote-first.

View jobs

Popular posts

Interactive stories (beta)

Simulate user behaviour using play functions
loading
Varun Vachhar
Join the community
6,543 developers and counting
WhyWhy StorybookComponent-driven UI
Open source software
Storybook

Maintained by
Chromatic
Special thanks to Netlify and CircleCI