
Storybook 按需架构
构建的 Storybooks 减小 3 倍,加载速度提升更快

随着故事(stories)数量的增长,如何高效地加载它们变得越来越困难,这会拖慢开发者的体验。我们使用 Storybook 来构建 Storybook,所以我们也能体会到这种痛苦。
在最近的几个版本中,性能已经成为首要任务。最新版本在构建时间和打包大小方面都取得了渐进式但显著的改进。
我很高兴地与大家分享 Storybook 的新“*按需加载架构*”,这是 6.4 版本即将迎来的一项根本性改变,它将提高已构建 Storybook 的性能。我们与 Webpack 和 Shopify UX 工程团队合作,将打包大小减少了高达三倍。继续阅读以了解详情。
幕后
在开始之前,让我们回顾一下幕后的工作原理。Storybook 是由一组称为“故事”的组件示例组成。这些是小的 Javascript 代码片段,用于在隔离环境中渲染 UI 组件(通常是设计系统或应用程序的一部分)。
故事定义在 CSF 文件(组件故事格式)中。与组件相关的所有故事都分组在同一个文件中。Storybook 的工作是获取 CSF 文件中定义的故事,并在用户请求时将其渲染到浏览器中。
为了渲染这些示例,我们需要在浏览器中加载所有相关的代码。为此,我们使用 Webpack 创建一个 JavaScript 包,其中包含:所有 CSF 文件、你的组件以及渲染它们所需的资源。外加 Storybook 的运行时。
由于打包大小对性能有巨大影响,因此我们将精力集中在缩小它。

如何将 Storybook 的打包大小减半
在性能方面,更小 = 更快。Storybook 包越小,加载速度就越快。考虑到这一点,我们进行了两项架构更改,以提高开发体验的速度:
- 代码分割:为已发布的 Storybook 实现更快的加载时间。
- 智能文件系统缓存:实现更快的开发启动速度。
在之前的 Storybook 版本中,所有代码都被打包到一个大包中。更多的组件和故事会导致更重的包,从而减慢 Storybook 的速度。启动(尤其是在网络加载时)或启动开发服务器需要花费一些时间。
近年来,大型应用程序开始依赖打包分割。其思想是将大包分割成更小、更易于管理的块。此外,像 NextJS 这样的工具率先采用了延迟编译技术。在启动时构建整个应用程序需要时间。相反,它们只构建用户关注的特定任务所需的模块。
打包分割的关键在于仅加载第一次渲染所需代码。其他所有内容都将在需要时通过异步方式(通过 import() 构造)获取。
应用程序通过手动指定并等待 import(),或者像 NextJS 那样根据页面路由自动分割来实现。第一种选择更手动,并提供更多对体验的控制。第二种选择可以在框架层面进行优化,但通常会限制你可以做什么。
对于 Storybook,我们与 Webpack 团队合作探索了这两种方法。
无效的方法:手动 import() 函数
自 Storybook 6.1 以来,可以使用 import() 函数对 Storybook 进行代码分割——通过使用实验性功能: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' }
};在上面的 CSF 文件中,没有直接静态 import ./MyComponent;而只是在提供的加载器中进行了异步 import()。
通过这种设置,所有 CSF 文件会创建一个单一的(初始)包。而每个组件文件将形成自己的包,以及它的依赖项。
在原型设计此方法时,我们发现了两个主要缺点:
- 用户需要为每个组件编写加载器,这既繁琐又不直观,并且使重用故事变得更加困难。代码分割似乎是一个优化细节,而你作为 Storybook 的用户不应该关心它。
- 在实验中,我们经常发现包含所有 CSF 文件的初始包占 Storybook 总大小的很大一部分,从而降低了代码分割的好处。
那个大的初始包通常是因为很难使 CSF 保持“纯粹”而不包含其他组件依赖项。此外,最小化初始包大小在 Storybook 被嵌入或组合到其他上下文中的情况下尤其重要。
有效的方法:自动代码分割
另一种方法是让 Storybook 的 store 将每个 CSF 文件视为单独的异步 import(),并“按需”加载故事。
这样,每个 CSF 都会生成自己的包——组件加上加载和渲染故事所需的最低依赖项。无需更改代码。所有这些都在后台发生,无需用户干预。
这种方法更复杂,并会导致一些限制(如下文所述)。但通常适用于最复杂的 Storybook,只需最少的更改。
此行为将在 Storybook 7.0 中成为默认行为——它可以通过 storyStoreV7 功能标志(如下文所述)在 6.4 中使用;6.4 的安装中默认仍然启用以前的单包行为。
6.4 中的性能优势
引入代码分割的主要目的是提高 Storybook 的性能。也就是说,安装和启动所需的时间,以及已发布的 Storybook 的下载和交互时间。
6.4 版本中的更改侧重于在 Storybook 中启用代码分割。直接影响将是更小的打包大小,这意味着已发布的 Storybook 应该会加载得更快。
例如,Chromatic Storybook(一个拥有 2000 个故事的大型 Storybook)在升级到 v7 store 后显示了以下行为:

同样,Shopify 的 Storybook 在启用 v7 store 后,初始打包大小节省了 67.5%。

6.5+ 的下一步性能改进
代码分割本身并不一定能改善使用 Storybook 的开发体验。生成多个代码分割包甚至可能比创建一个大包花费更长的时间。这取决于包之间的代码重复以及优化内容的复杂性。
但是,它为进一步优化提供了可能。其中一个关键是使用延迟编译,只生成渲染屏幕上当前可见的故事所需的包。延迟编译是 Webpack 5 的一项实验性功能,概念上类似于 NextJS 的即时页面构建。
延迟编译和文件系统缓存的实验表明,在大型项目中,开发启动时间和重建时间应该可以减少 3-5 倍。这将是 Storybook 6.5 的主要重点。
此外,现在还解锁了对 Webpack 分割机制的其他优化。我们鼓励用户尝试调整 Storybook 的默认 Webpack 设置,并将改进贡献回 6.5 版本。
注意事项
自动代码分割方法的棘手之处在于,我们不再在“启动”时加载所有 CSF 文件。相反,我们需要从节点上下文中静态计算 Storybook 的故事列表(“故事索引”)。这意味着我们不评估你的故事文件,而只是解析它们并分析生成的 AST。这会限制你在 CSF 文件中的操作(我们可能会在未来的迭代中移除其中一些限制)。
- 仅支持 CSF 格式(v1-v3);不支持
storiesOf()。 - CSF 标题和故事名称必须是静态定义的(例如
title: 'Component',而不是title: MyTitle)。 - 自定义
storySort函数提供了更有限的 API。
立即试用
自动代码分割现已在 6.4 Beta 版中提供。只需一分钟即可试用,你可以在项目根目录下运行以下命令:
npx sb upgrade --prerelease如果你还没有使用 Storybook,入门非常简单:
npx sb@next init然后启用功能标志:
// .storybook/main.js
module.exports = {
features: {
storyStoreV7: true,
}
};
帮助塑造下一代 Storybook!
Storybook 的按需加载架构带来了显著的性能优势,并允许你尝试其他 Webpack 优化。
开发者每天使用 Storybook 构建数百个组件和数千个故事。你对加速 Storybook 做过哪些调整?我们很想听听你的看法。请在 Twitter 上联系我们,或访问 Storybook Discord。
按需加载架构功能由Tom Coleman(我!)、Juho Vepsäläinen 和Michael Shilman 在整个 Storybook 社区的反馈下开发的。
Storybook 是由 1320 多名社区贡献者共同打造的产品,并由顶级维护者组成的指导委员会组织。你也可以贡献新功能、修复 bug 或改进文档。加入我们的 Discord,在 Open Collective 上支持我们,或者直接参与 GitHub。
3倍更小,4倍更快!
— Storybook (@storybookjs) 2021年10月13日
Storybook 6.4 为静态构建的 Storybook 带来了巨大的性能提升。
通过自动代码分割,它只加载第一个故事所需的代码。
其影响——更小的打包大小和更快的加载时间。https://#/K5F5D1ZhW8 pic.twitter.com/37Kw6z7T97