当代码库达到一定规模后,在每次提交前进行完整的 TypeScript 类型检查会非常耗时,就像下面的代码库,执行完整的检查需要 47s,如果每次代码提交时执行类型检查,开发者都得等上接近 1 分钟才能完成 git commit,极大的拉低了开发体验。
───────────────────────────────────────────────────────────────────────────────
Language Files Lines Blanks Comments Code Complexity
───────────────────────────────────────────────────────────────────────────────
TypeScript 3721 271374 23335 21340 226699 16639
❯ nx run-many -t check --all
✔ nx run icons:check (3s)
✔ nx run visual:check (4s)
✔ nx run eslint-plugin-merico:check (4s)
✔ nx run vdev-api:check (176ms)
✔ nx run nx-plugin:check (3s)
✔ nx run illustrations:check (4s)
✔ nx run shared-lib_temp:check (4s)
✔ nx run fekit:check (12s)
✔ nx run charts:check (14s)
✔ nx run main:check (39s)
—————————————————————————————
> NX Successfully ran target check for 10 projects (47s)
那么为什么会这么慢呢?责任真的全在 nodejs 的性能上?
为什么这么慢
TypeScript 文档中这样写道:
Rather than doing a full check of all
d.ts
files, TypeScript will type check the code you specifically refer to in your app’s source code.
所以当你的 monorepo 中存在项目间引用的时候,执行 tsc -p lib-bob/tsconfig.json
会同时在 lib-bob
及其依赖的 lib-alice
上运行类型检查,这意味着,如果你在 monorepo 中对所有项目分别执行 tsc -p xx/tsconfig.json
,被其他项目依赖的项目,会被重复检查多次!
为了避免这种问题,TypeScript 的 build mode 提供了一种基于增量构建的解决方案。启用 build mode 后,庞大的代码库可以被拆分成多个项目分别被 tsc
构建,tsc
会缓存每个项目的构建结果,当一个项目被其他项目引用的时候,这个项目不会被重新构建而是使用之前的构建结果。在后续的执行中,只有发生变动的项目才会被重新构建。
如何配置 build mode
启用 build mode 的方法比较简单,你只需要调整 tsconfig.json 中以下几个选项即可:
compilerOptions.composite: true
,用来标记这个项目可以被增量构建compilerOptions.noEmit: false
,启用增量构建必须允许tsc
生成文件compilerOptions.outDir: path/to/tsc-out
,设置生成文件(包括缓存文件)的路径,记得加入gitignore
compilerOptions.emitDeclarationOnly: true
,如果你使用tsc
构建 js 文件,可以不设置这个选项references
,把依赖项目的 tsconfig 路径添加到这里,让tsc
可以使用这些依赖项目的构建缓存compilerOptions.rootDir
,不建议设置,避免构建缓存生成到outDir
目录之外
现在,你就可以使用 tsc -b
(注意,不是 tsc -p
) 来构建你的项目了,同样还是开头展示的代码库,现在重新运行类型检查:
❯ time nx run-many -t check --all --skip-nx-cache
✔ nx run illustrations:check (3s)
✔ nx run icons:check (5s)
✔ nx run visual:check (5s)
✔ nx run shared-lib_temp:check (781ms)
✔ nx run vdev-api:check (5s)
✔ nx run eslint-plugin-merico:check (832ms)
✔ nx run nx-plugin:check (2s)
✔ nx run fekit:check (10s)
✔ nx run fekit-e2e:check (2s)
✔ nx run charts:check (12s)
✔ nx run main:check (27s)
——————————————————————————————
> NX Successfully ran target check for 11 projects (44s)
________________________________________________________
Executed in 44.50 secs fish external
usr time 119.40 secs 213.00 micros 119.40 secs
sys time 6.17 secs 67.00 micros 6.17 secs
看起来似乎长进不大,再运行一遍试试?
❯ time nx run-many -t check --all --skip-nx-cache
✔ nx run illustrations:check (752ms)
✔ nx run visual:check (760ms)
✔ nx run icons:check (768ms)
✔ nx run shared-lib_temp:check (732ms)
✔ nx run fekit:check (880ms)
✔ nx run vdev-api:check (1s)
✔ nx run eslint-plugin-merico:check (731ms)
✔ nx run charts:check (900ms)
✔ nx run nx-plugin:check (756ms)
✔ nx run fekit-e2e:check (773ms)
✔ nx run main:check (1s)
—————————————————————————————
> NX Successfully ran target check for 11 projects (4s)
________________________________________________________
Executed in 4.49 secs fish external
usr time 13.51 secs 0.00 micros 13.51 secs
sys time 1.86 secs 369.00 micros 1.86 secs
类型检查速度变得飞快,这是因为 tsc
复用了第一次生成的构建缓存。如果你这时检查 outDir
,你就会发现除了 d.ts
之外,tsc
还生成了 tsbuildinfo
文件,这就是 tsc
用来判断是否可以复用 d.ts
的关键。
❯ ls dist/out-tsc/icons
components tsconfig.build.tsbuildinfo
可以想象的是,在平常开发过程中,每个 commit 通常只会修改一小部分文件,如果我们把代码库中较大的项目(例如上面的 main 项目,代码量有 18w 行)进一步拆分,就可以更加充分地利用增量构建的特性,运行类型检查的范围就会更小,速度就会更快。
维护 monorepo 中的项目依赖
细粒度的拆分项目在提高类型检查速度的同时,又会带来一个新的问题,由开发者人工维护项目之间的 references
字段会变得很麻烦,而且容易出错。为了减少人工操作的负担,我开发了一个小工具 —— ts-sync-ref,它会分析项目的 monorepo 内依赖,进而更新 tsconfig.json 的 references
字段。
# 安装 ts-sync-ref
npm install -g @zeeko/ts-sync-ref
# 更新 my-lib 的依赖到 tsconfig.json 中的 references 字段
cd /path/to/my-monorepo
ts-sync-ref -p packages/my-lib/tsconfig.json -f 'packages/my-lib/src/**/*.ts'
你可以阅读 ts-sync-ref 的说明文档、代码,进一步了解使用方式及其实现原理。
结论
Monorepo 的组织方式有很多种,不管你在用什么工具管理你的 TypeScript monorepo,不妨试试通过 TypeScript 的 Project Reference 将大代码库拆分成多个项目,不仅可以提升类型检查、构建速度,还可以减少编辑器的内存占用,极大地提高开发体验。
不过,需要注意的是,直接启用 Project Reference 可能会给你的项目带来一些影响,导致现有的构建脚本出错,例如:
最后,如果你有兴趣的话,也欢迎你在评论区分享你的项目执行类型检查的方案。
发表回复