如何减少 monorepo 中 lib 的初始化时间
本文最后更新于:2021年11月14日 下午
场景
在我们使用 monorepo 将所有的前端项目放到一个项目中后,会面临各种各样的问题,其中的多个通用模块的初始化也会是一个问题。
下面是目前实践过的一些解决方案
- 增量构建
- 捆绑依赖项
- 基于 esbuild
- 不构建 dts
增量构建
每次修改 libs 中的内容,其他人通过 git 拉取时都需要重新 initialize
,如果知道在哪个包还好,可以仅运行指定包的 initialize
命令。如果不知道的话,则需要运行根目录的 initialize
命令,这其实是非常缓慢的,因为它会重新运行所有包含 intialize
命令的 npm 包,而不管它们是否有变更。
- 减少 initialize 时间,提高协作的开发体验
- 支持 ci/cd 缓存已构建的 libs,加快构建时间
需求
- 尽可能地按照依赖图并发执行命令,并且基于 git 变更实现缓存
- 在指定模块依赖的模块执行命令
- 在所有子模块中执行命令
真实项目构建时间
lerna run
- 新项目首次初始化 198.54s
- 非新项目再次初始化 90.17s
吾辈在 yarn-plugin-changed 模块中基于 yarn2 实现了这个功能。
捆绑依赖项
加载一个文件总比加载 100 个小文件要快,这也是为什么 webpack 等工具会将开发的代码打包,yarn2 推动 pnp 的主要原因之一。对于 cli 而言,也是一样的道理,将依赖尽可能地打包到 bundle 中就好了。虽然打包本身会增加一点时间,但在其他模块使用 cli 时就会快一些。
1 |
|
44.142s => 30s 438ms
基于 esbuild
rollup 使用 js 编写,它在使用必须的插件之后打包非常缓慢(可能部分要归结于 tsc 本身也非常慢),而 esbuild 在官方性能测试中要快 10-100 倍,这为性能优化提供了一种思路:将 CPU 密集型的功能使用高性能的语言构建。在使用 esbuild 命令行时,基本上,lib/cli 都能在数百毫秒内完成构建,而其中实际运行构建代码的时间大约只有几十毫秒,大部分时间是在等待 cli 启动。
下面是一个在单模块的性能测试
esbuild cli
1 |
|
esbuild base nodejs cli
1 |
|
不构建 dts
为什么不构建 dts?
构建 dts 很慢,至于多慢呢?下面是一个构建 cli 的时间分析
不生成类型定义
1 |
|
生成类型定义
1 |
|
可以看到,生成类型定义耗费了大量的时间,与使用 esbuild 构建完全不在一个时间量级上。但同时,我们也必须注意到,nodejs 花费的真实时间很多,甚至远超构建本身,也就意味着,nodejs 确实存在性能极限。实际项目中,初始化 15 个模块需要 30s 438ms,而生成 dts 则需要 44s 343ms,单点优化对整体已经很难产生数量级的影响了。
问题
- 通用模块中的 ts 的错误会蔓延到业务模块中,主要是在自定义
src/@types/
类型的时候
不做任何打包
虽然看起来不可思议,但在 monorepo 中,许多通用模块可能并不需要在 monorepo 之外使用到,所以对于有些不需要发布的模块,可以不做打包,而是直接在 package.json 中指向未打包的入口文件。
下面是一个 lib 的不打包配置
1 |
|
当然,它也会带来一些副作用,例如
- 使用该模块必须支持 ts
- 增加依赖该模块的终端程序的打包时间