给大代码库的类型检查提速


这是一系列博文中的一篇,用来记录将我将公司的前端项目改造为 monorepo 的过程。

4 / 4 of Nx Monorepo Experience

当代码库达到一定规模后,在每次提交前进行完整的 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 可能会给你的项目带来一些影响,导致现有的构建脚本出错,例如:

最后,如果你有兴趣的话,也欢迎你在评论区分享你的项目执行类型检查的方案。

Series Navigation<< 如何使用 nx 缓存构建产物
,

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

You can use markdown syntax in comment