MM

Faster interactive development in Storybook

(The following is expanded from my comment at #storybook/10987)

Storybook provides, among other things, a canvas to develop UI components interactively and in isolation.

In any kind of digital work, interactivity comes not only from the absence of manual steps between making a change and seeing the outcome, but also from a feedback loop tight enough to keep your mind on the task: a word processor that takes five seconds to turn a word to italic loses both the semblance and the benefits of interactivity.

Mostly due to the underlying toolchain, that’s exactly the situation Storybook can find itself in: a freshly initialized Storybook in a freshly initialized React app can, depending on machine, take more than two seconds to refresh the example button after changing its size from “medium” to “large”. I’ve seen that grow to 5+ seconds even for small (<5k LOC) projects.

These tweaks can bring those numbers down to more workable levels.

Optimization #1: disable plugins irrelevant to interactive development #

Some plugins either create value in areas other than interactive development (such as documentation) or replicate work already done in editors (linting, typechecking). That can take a lot of time. See for example the impact of the Docgen plugin (each timestamp is the time elapsed since the previous line):

~/.../src: yarn storybook | gnomon
...
0.1254s <s> [webpack.Progress] 70% finish module graph ESLintWebpackPlugin
<s> [webpack.Progress] 70% finish module graph FlagDependencyExportsPlugin
...
0.0096s <s> [webpack.Progress] 70% sealing WarnCaseSensitiveModulesPlugin
1.0012s <s> [webpack.Progress] 70% sealing React Docgen Typescript Plugin
1.5016s <s> [webpack.Progress] 70% sealing React Docgen Typescript Plugin
2.0022s <s> [webpack.Progress] 70% sealing React Docgen Typescript Plugin
2.3576s <s> [webpack.Progress] 70% sealing React Docgen Typescript Plugin
<s> [webpack.Progress] 72% basic dependencies optimization
0.0002s <s> [webpack.Progress] 72% basic dependencies optimization

Plugins can be disabled in .storybook/main.js:

  const { inspect } = require('util')

module.exports = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/preset-create-react-app',
],

+ typescript: {
+ check: false,
+ reactDocgen: false,
+ },
+
+ webpackFinal: async (config, { configType }) => {
+ config.plugins = config.plugins.filter(
+ (p) => !inspect(p).match(/^ESLintWebpackPlugin/),
+ )
+ return config
+ },
}

If you’re also publishing the Storybook output as documentation/style guide/showcase, you’ll want to do the above only for NODE_ENV=development.

Optimization #2: replace stages of the toolchain with faster ones #

Note: This one appears to no longer work with the current release of esbuild-loader.

Though primarily meant to reduce cold build times, the storybook-addon-turbo-build add-on does further shave time away from interactive refreshes. After installation, enable it with:

  module.exports = {
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/preset-create-react-app',
+ {
+ name: "storybook-addon-turbo-build",
+ options: { optimizationLevel: 3 },
+ },
],

Optimization #3: get (partially) rid of Webpack #

Snowpack is an alternative to Webpack that, through a radically different approach to bundling, provides instant refresh. It can be used in development while leaving the production build to a tried-and-tested Webpack configuration. Unfortunately, Storybook doesn’t run in Snowpack yet.

However, if the gains from optimization #1 and #2 are not enough (they weren’t for me), it’s worth to rethink the problem so that Snowpack can be brought in.

What is the core value of the Storybook canvas for interactive development? We already disabled documentation generation. Do you need widgets to update component props? Do you need add-ons to check the component on different backgrounds and with different locales? Much of that comes into play in a later, shorter “refinement” phase.

If you’ve adopted Snowpack for your dev environment, and can tolerate the trade-offs, you can create a /canvas route that only exists when NODE_ENV=development, where you develop components in semi-isolation, re-use Storybook stories, and benefit from instant refresh. The minimum viable version of it is:

src/Components/App.tsx:

  <Router>
<Switch>
<Route ... />
<Route ... />
{process.env.NODE_ENV === 'development' && (
<Route path="/canvas" component={Canvas} exact={true} />
)}

src/Components/Canvas.tsx:

import React from 'react'
import * as Button from './Button.stories'

export const Canvas: React.FC = () => {
const StoryBeingWorkedOn = Button.Basic
return (
<div>
<StoryBeingWorkedOn {...StoryBeingWorkedOn.args} />
</div>
)
}

(It’s just as easy to import multiple stories and add a way to switch among them at runtime. You don’t want to replicate the entire Storybook canvas, but a little convenience can go a long way.)

The only downsides of this strategy are 1) when you change component args in story files, the changes aren’t picked up and you need a full page reload; and 2) if you use decorators, you need to write some more support code.

References #