tsc性能优化Project References使用详解
目录
- 什么是 Project References
- 示例项目结构
- 不使用 Project References 带来的问题
- tsconfig.json 的 references 配置项
- tsconfig.json 的 composite 配置项
- 使用 Project References 改造示例项目
- 全量构建
- 增量构建
- 对__test__测试代码的处理
- 总结
什么是 Project References
在了解一个东西是什么的时候,直接看其官方定义是最直观的
TypeScript: Documentation中对于Project References的介绍如下:
Project references are a new feature in TypeScript 3.0 that allow you to structure your TypeScript programs into smaller pieces.
这是TypeScript 3.0新增的特性,这个特性有啥用呢?
将我们的项目分成多个小的片段,也就是允许我们将项目进行分包模块化
这样一来我们在执行tsc对项目进行构建的时候,无论是出于代码转译成js的目的还是说出于单纯的类型检查(将compilerOptions.noEmit置为true)的目的
都可以严格按照自己的需要对想要被tsc处理的那部分代码进行处理,而不是每次都对整个项目进行处理,这是我认为Project References的最大好处
就以一个前后端代码在同一个仓库里维护的项目为例,如果没有Project References,那么我执行tsc时,会对前后端模块都进行类型检查和转译
但实际上如果我只修改了前端部分的代码,理所应当让tsc只处理前端模块,后端模块的构建产物不需要进行重新构建,有了Project References,我们就能实现到这个需求,从而优化项目的构建或类型检查性能
相信大家对Project References有一个大概的认识了,接下来我们就开始实际动手体验一下Project References加深我们对它的理解吧!
示例项目结构
. ├── package.json ├── pnpm-lock.yaml ├── src │ ├── __test__ // 单元测试 │ │ ├── client.test.ts // 前端代码测试 │ │ ├── index.ts // 简陋的单元测试 API │ │ └── server.test.ts // 后端代码测试 │ ├── client // 前端模块 │ │ └── index.ts │ ├── server // 后端模块 │ │ └── index.ts │ └── shared // 共享模块 -- 包含通用的工具函数 │ └── index.ts └── tsconfig.json // TypeScript 配置
这是一个很常见的项目目录结构,有前端代码,有后端代码,也有通用工具函数代码以及前后端的单元测试代码
它们的依赖关系如下:
- client 依赖 shared
- server 依赖 shared
__test__依赖 client 和 server- shared 无依赖
不使用 Project References 带来的问题
现在整个项目只有一个tsconfig.json位于项目根目录下,其内容如下:
{
"compilerOptions": {
"target": "ES5",
"module": "CommonJS",
"strict": true,
"outDir": "./dist"
}
}
如果我们执行tsc,它会将各个模块的代码都打包到项目根目录的dist目录下
dist
├── __test__
│ ├── client.test.js
│ ├── index.js
│ └── server.test.js
├── client
│ └── index.js
├── server
│ └── index.js
└── shared
└── index.js
这有一个很明显的问题,正如前面所说,当我们只修改一个模块,比如只修改了前端模块的代码,那么理应只需要再构建前端模块的产物即可,但是无论改动范围如何,都是会将整个项目都构建一次,这在项目规模变得越来越大的时候会带来极大的性能问题,构建时长会变得特别长
或许你会想着在每个模块里创建一个tsconfig.json,然后通过tsc -p指定每个模块的目录去单独对它们进行构建,没错,这是一种解决方案
但是这会带来下面两个问题:
- 如果需要全量构建项目,你得需要运行三次
tsc,对每个模块分别构建,而tsc的启动时间开销是比较大的,在这个小规模项目里甚至启动开销的时间比实际构建的时间更长,现在还只是运行三次tsc,如果项目模块很多,有几十上百个呢?那光是启动tsc几十上百次都已经会花一些时间了 tsc -w不能一次监控多个tsconfig.json,只能是对各个模块都启动一次tsc -w
Project References的出现,就是为了解决上述问题的
tsconfig.json 的 references 配置项
Project References就是tsconfig.json里的references配置项,其结构是一个包含若干对象的数组,对象的结构如下:
{
"references": [{ "path": "path/to/referenced-project" }]
}
核心就是一个path属性,该属性指向被引用的项目模块路径,该路径下需要包含tsconfig.json,如果该模块不是用tsconfig.json命名的话,你也可以指定具体的文件名,比如:
{
"references": [{ "path": "path/to/referenced-project/tsconfig.web.json" }]
}
当指定了references选项后,会发生如下改变:
- 在主模块中导入被引用的模块时,会加载它的类型声明文件,也就是
.d.ts后缀的文件 - 使用
tsc --build或tsc -b构建主模块时,会自动构建被引用的模块
这样一来能够带来三个好处:
- 提升类型检查和构建的速度
- 减少
IDE的运行内存占用 - 更容易对项目结构进行划分
tsconfig.json 的 composite 配置项
光是在主模块中指定references配置项还不够,还需要在被引用的项目对应的tsconfig.json中开启composite配置项
composite配置项又是干嘛的呢? -- 它可以帮助tsc快速确定如何寻找被引用项目的输出产物
当被引用的项目开启composite配置项后,会有如下改变和要求:
当未指定rootDir时,默认值不再是The longest common path of all non-declaration input files,而是包含了tsconfig.json的目录
Tips: 关于The longest common path of all non-declaration input files的意思可以到tsconfig.json 文章中关于 rootDir 的介绍中查阅
必须开启include或者files配置项将要参与构建的文件声明进来
必须开启declaration配置项(因为前面介绍references的时候说了,会加载被引入模块的类型声明文件,因此被引用模块自然得开启declaration配置项生成自己的类型声明文件供主模块加载)
使用 Project References 改造示例项目
根据目前我们对Project References的认识,现在可以开始改造一下我们的项目了,首先是根目录下的tsconfig.json配置,它起到一个类似于项目入口的作用,因此这里面只负责添加references声明项目中需要被构建的模块,以及通过exclude将不需要参与构建的模块排除(比如src/__test__中的测试代码)
/tsconfig.json
{
"references": [
{ "path": "src/client" },
{ "path": "src/server" },
{ "path": "src/shared" }
],
"exclude": ["**/__test__"]
}
然后是各个子模块的tsconfig.json配置,这里我们假设构建目标为es5的代码,所以对于client、server以及shared来说是存在公共配置的,所以我们可以抽离出一个公共配置,然后在子模块中通过extends配置项公用一个配置
/tsconfig.base.json
{
"compilerOptions": {
"target": "ES5",
"module": "CommonJS",
"strict": true
}
}
src/client/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "../../dist/client",
"composite": true,
"declaration": true
},
// 依赖哪个模块则引用哪个模块
"references": [{ "path": "../shared" }]
}
src/server/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "../../dist/server",
"composite": true,
"declaration": true
},
// 依赖哪个模块则引用哪个模块
"references": [{ "path": "../shared" }]
}
src/shared/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "../../dist/shared",
"composite": true,
"declaration": true
}
}
全量构建
现在我们在项目根目录下运行tsc --build --verbose,就会根据references配置去寻找各个子模块,并对它们进行构建,可以理解为对项目的全量构建
--build 参数表示让tsc以build模式进行构建和类型检查,也就是会使用references配置项,如果不开启的话是不会使用references配置项的,这点可以从官方文档中得证:

--verbose 参数则是会将构建过程中的输出显示在控制台中,不开启该参数的话则不会显示输出(除非构建过程中报错)
运行后/dist目录结构如下
dist
├── client
│ ├── index.d.ts
│ ├── index.js
│ └── tsconfig.tsbuildinfo
├── server
│ ├── index.d.ts
│ ├── index.js
│ └── tsconfig.tsbuildinfo
└── shared
├── index.d.ts
├── index.js
└── tsconfig.tsbuildinfo
可以看到,所有子模块都被构建进来了,各模块的产物中有一个tsconfig.tsbuildinfo文件,这个文件起到一个类似缓存的作用,对于后续进行增量构建有着重要作用
前面看到的官方文档中对tsc --build的作用的介绍中的第二点Detect if they are up-to-date主要就是依靠这个缓存文件去识别的
开启--verbose参数后,可以看到控制台输出如下:
[4:50:26 PM] Projects in this build:
* src/shared/tsconfig.json
* src/client/tsconfig.json
* src/server/tsconfig.json
* tsconfig.json
[4:50:26 PM] Project 'src/shared/tsconfig.json' is out of date because output file 'dist/shared/tsconfig.tsbuildinfo' does not exist
[4:50:26 PM] Building project '/home/plasticine/demo/ts-reference-demo/src/shared/tsconfig.json'...
[4:50:28 PM] Project 'src/client/tsconfig.json' is out of date because output file 'dist/client/tsconfig.tsbuildinfo' does not exist
[4:50:28 PM] Building project '/home/plasticine/demo/ts-reference-demo/src/client/tsconfig.json'...
[4:50:28 PM] Project 'src/server/tsconfig.json' is out of date because output file 'dist/server/tsconfig.tsbuildinfo' does not exist
[4:50:28 PM] Building project '/home/plasticine/demo/ts-reference-demo/src/server/tsconfig.json'...
[4:50:29 PM] Project 'tsconfig.json' is out of date because output 'src/shared/index.js' is older than input 'src/client'
[4:50:29 PM] Building project '/home/plasticine/demo/ts-reference-demo/tsconfig.json'...
[4:50:29 PM] Updating unchanged output timestamps of project '/home/plasticine/demo/ts-reference-demo/tsconfig.json'...
xxx out of date xxx意思就是这个模块没被构建过,因此会开始对其进行构建
由于我们是首次构建,所以三个模块都是没被构建过的,所以三个模块都被检测为out of date
当我们再次运行tsc --build --verbose时,输出如下:
4:54:35 PM - Projects in this build:
* src/shared/tsconfig.json
* src/client/tsconfig.json
* src/server/tsconfig.json
* tsconfig.json
4:54:35 PM - Project 'src/shared/tsconfig.json' is up to date because newest input 'src/shared/index.ts' is older than output 'dist/shared/tsconfig.tsbuildinfo'
4:54:35 PM - Project 'src/client/tsconfig.json' is up to date because newest input 'src/client/index.ts' is older than output 'dist/client/tsconfig.tsbuildinfo'
4:54:35 PM - Project 'src/server/tsconfig.json' is up to date because newest input 'src/server/index.ts' is older than output 'dist/server/tsconfig.tsbuildinfo'
4:54:35 PM - Project 'tsconfig.json' is up to date because newest input 'dist/server/index.d.ts' is older than output 'src/client/index.js'
可以看到,所有模块都被检测为up to date,从而避免了重复构建
增量构建
如果现在我们修改了client模块的代码,再运行tsc --build --verbose会怎样呢?估计你也能猜到了,只有client模块会被构建,而其他模块则会跳过
4:56:44 PM - Projects in this build:
* src/shared/tsconfig.json
* src/client/tsconfig.json
* src/server/tsconfig.json
* tsconfig.json
4:56:44 PM - Project 'src/shared/tsconfig.json' is up to date because newest input 'src/shared/index.ts' is older than output 'dist/shared/tsconfig.tsbuildinfo'
4:56:44 PM - Project 'src/client/tsconfig.json' is out of date because output 'dist/client/tsconfig.tsbuildinfo' is older than input 'src/client/index.ts'
4:56:44 PM - Building project '/home/plasticine/demo/ts-reference-demo/src/client/tsconfig.json'...
4:56:45 PM - Project 'src/server/tsconfig.json' is up to date because newest input 'src/server/index.ts' is older than output 'dist/server/tsconfig.tsbuildinfo'
4:56:45 PM - Project 'tsconfig.json' is out of date because output file 'src/client/index.js' does not exist
4:56:45 PM - Building project '/home/plasticine/demo/ts-reference-demo/tsconfig.json'...
4:56:45 PM - Updating unchanged output timestamps of project '/home/plasticine/demo/ts-reference-demo/tsconfig.json'...
相信现在你能体会到Project References的好处了吧,能够很大程度上优化我们的构建速度!
不过实际开发中,tsc更多的是用来进行类型检查,至于compile的工作,则更多地是交给如Babel、swc、esbuild等工具去完成,这也是官方文档中有提到过的

这也是为什么你在vite创建的项目中能够看到默认的build命令配置为tsc && vite build,正是将类型检查的工作交给tsc,而构建工作则交给vite底层依赖的rollup去完成
对__test__测试代码的处理
我们的改造貌似已经完成了,但其实还忽略了一个src/__test__,它也可以被视为一个模块,它作为主模块,依赖了client和server,因此也可以给它加上tsconfig.json配置,并且对于测试代码,我们一般不希望将它们构建成js,只希望tsc负责类型检查的工作,因此我们需要进行如下配置:
src/__test__/tsconfig.json
{
"compilerOptions": {
"noEmit": true
},
"references": [{ "path": "../client" }, { "path": "../server" }]
}
noEmit的作用刚刚在官方文档中也看到了,不会把产物文件输出,如果我们只需要类型检查能力的话很适合开启该配置项
现在我们如果需要对__test__中的代码进行类型检查的话,只需要执行:
# 忽略 references 配置项 tsc --project src/__test__ # 启用 references 配置项 tsc --build src/__test__
如果是使用--project参数的话,tsconfig.json中可以忽略references配置项,因为即便配置了也不会被使用,这在依赖产物未构建出来时能起作用
而如果使用--build参数,并且client和server未构建出来时,会先构建它们,再对测试代码进行类型检查,可以根据个人需求场景来决定使用--project还是--build
总结
本篇文章介绍了Project References是什么,并通过一个简单的示例项目,并结合TypeScript Documentation官方文档边实战边解释
总的来说,其使用起来就是:
- 主模块(
tsc --build作用的模块视为主模块)中通过references配置项声明依赖的模块 - 被引用模块中开启
composite和declaration配置项以支持被引用 - 通过
tsc --build 主模块才可以启用references配置项,这在官方文档中被称为Build Mode,如果直接tsc 主模块的话,是不会启用references配置项的,也就导致依然会对项目中的所有ts文件进行编译(如果没配置include或files配置项的话)
希望通过本篇文章,能够让你对Project References有一个全面了解,也希望能够将其用在你的项目中,提升类型检查或构建(使用 tsc 进行构建的话)的速度,更多关于tsc性能Project References的资料请关注我们其它相关文章!
