tsc性能優(yōu)化Project References使用詳解
什么是 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新增的特性,這個特性有啥用呢?
將我們的項(xiàng)目分成多個小的片段,也就是允許我們將項(xiàng)目進(jìn)行分包模塊化
這樣一來我們在執(zhí)行tsc對項(xiàng)目進(jìn)行構(gòu)建的時候,無論是出于代碼轉(zhuǎn)譯成js的目的還是說出于單純的類型檢查(將compilerOptions.noEmit置為true)的目的
都可以嚴(yán)格按照自己的需要對想要被tsc處理的那部分代碼進(jìn)行處理,而不是每次都對整個項(xiàng)目進(jìn)行處理,這是我認(rèn)為Project References的最大好處
就以一個前后端代碼在同一個倉庫里維護(hù)的項(xiàng)目為例,如果沒有Project References,那么我執(zhí)行tsc時,會對前后端模塊都進(jìn)行類型檢查和轉(zhuǎn)譯
但實(shí)際上如果我只修改了前端部分的代碼,理所應(yīng)當(dāng)讓tsc只處理前端模塊,后端模塊的構(gòu)建產(chǎn)物不需要進(jìn)行重新構(gòu)建,有了Project References,我們就能實(shí)現(xiàn)到這個需求,從而優(yōu)化項(xiàng)目的構(gòu)建或類型檢查性能
相信大家對Project References有一個大概的認(rèn)識了,接下來我們就開始實(shí)際動手體驗(yàn)一下Project References加深我們對它的理解吧!
示例項(xiàng)目結(jié)構(gòu)
. ├── package.json ├── pnpm-lock.yaml ├── src │ ├── __test__ // 單元測試 │ │ ├── client.test.ts // 前端代碼測試 │ │ ├── index.ts // 簡陋的單元測試 API │ │ └── server.test.ts // 后端代碼測試 │ ├── client // 前端模塊 │ │ └── index.ts │ ├── server // 后端模塊 │ │ └── index.ts │ └── shared // 共享模塊 -- 包含通用的工具函數(shù) │ └── index.ts └── tsconfig.json // TypeScript 配置
這是一個很常見的項(xiàng)目目錄結(jié)構(gòu),有前端代碼,有后端代碼,也有通用工具函數(shù)代碼以及前后端的單元測試代碼
它們的依賴關(guān)系如下:
- client 依賴 shared
- server 依賴 shared
__test__依賴 client 和 server- shared 無依賴
不使用 Project References 帶來的問題
現(xiàn)在整個項(xiàng)目只有一個tsconfig.json位于項(xiàng)目根目錄下,其內(nèi)容如下:
{
"compilerOptions": {
"target": "ES5",
"module": "CommonJS",
"strict": true,
"outDir": "./dist"
}
}
如果我們執(zhí)行tsc,它會將各個模塊的代碼都打包到項(xiàng)目根目錄的dist目錄下
dist
├── __test__
│ ├── client.test.js
│ ├── index.js
│ └── server.test.js
├── client
│ └── index.js
├── server
│ └── index.js
└── shared
└── index.js
這有一個很明顯的問題,正如前面所說,當(dāng)我們只修改一個模塊,比如只修改了前端模塊的代碼,那么理應(yīng)只需要再構(gòu)建前端模塊的產(chǎn)物即可,但是無論改動范圍如何,都是會將整個項(xiàng)目都構(gòu)建一次,這在項(xiàng)目規(guī)模變得越來越大的時候會帶來極大的性能問題,構(gòu)建時長會變得特別長
或許你會想著在每個模塊里創(chuàng)建一個tsconfig.json,然后通過tsc -p指定每個模塊的目錄去單獨(dú)對它們進(jìn)行構(gòu)建,沒錯,這是一種解決方案
但是這會帶來下面兩個問題:
- 如果需要全量構(gòu)建項(xiàng)目,你得需要運(yùn)行三次
tsc,對每個模塊分別構(gòu)建,而tsc的啟動時間開銷是比較大的,在這個小規(guī)模項(xiàng)目里甚至啟動開銷的時間比實(shí)際構(gòu)建的時間更長,現(xiàn)在還只是運(yùn)行三次tsc,如果項(xiàng)目模塊很多,有幾十上百個呢?那光是啟動tsc幾十上百次都已經(jīng)會花一些時間了 tsc -w不能一次監(jiān)控多個tsconfig.json,只能是對各個模塊都啟動一次tsc -w
Project References的出現(xiàn),就是為了解決上述問題的
tsconfig.json 的 references 配置項(xiàng)
Project References就是tsconfig.json里的references配置項(xiàng),其結(jié)構(gòu)是一個包含若干對象的數(shù)組,對象的結(jié)構(gòu)如下:
{
"references": [{ "path": "path/to/referenced-project" }]
}
核心就是一個path屬性,該屬性指向被引用的項(xiàng)目模塊路徑,該路徑下需要包含tsconfig.json,如果該模塊不是用tsconfig.json命名的話,你也可以指定具體的文件名,比如:
{
"references": [{ "path": "path/to/referenced-project/tsconfig.web.json" }]
}
當(dāng)指定了references選項(xiàng)后,會發(fā)生如下改變:
- 在主模塊中導(dǎo)入被引用的模塊時,會加載它的類型聲明文件,也就是
.d.ts后綴的文件 - 使用
tsc --build或tsc -b構(gòu)建主模塊時,會自動構(gòu)建被引用的模塊
這樣一來能夠帶來三個好處:
- 提升類型檢查和構(gòu)建的速度
- 減少
IDE的運(yùn)行內(nèi)存占用 - 更容易對項(xiàng)目結(jié)構(gòu)進(jìn)行劃分
tsconfig.json 的 composite 配置項(xiàng)
光是在主模塊中指定references配置項(xiàng)還不夠,還需要在被引用的項(xiàng)目對應(yīng)的tsconfig.json中開啟composite配置項(xiàng)
composite配置項(xiàng)又是干嘛的呢? -- 它可以幫助tsc快速確定如何尋找被引用項(xiàng)目的輸出產(chǎn)物
當(dāng)被引用的項(xiàng)目開啟composite配置項(xiàng)后,會有如下改變和要求:
當(dāng)未指定rootDir時,默認(rèn)值不再是The longest common path of all non-declaration input files,而是包含了tsconfig.json的目錄
Tips: 關(guān)于The longest common path of all non-declaration input files的意思可以到tsconfig.json 文章中關(guān)于 rootDir 的介紹中查閱
必須開啟include或者files配置項(xiàng)將要參與構(gòu)建的文件聲明進(jìn)來
必須開啟declaration配置項(xiàng)(因?yàn)榍懊娼榻Breferences的時候說了,會加載被引入模塊的類型聲明文件,因此被引用模塊自然得開啟declaration配置項(xiàng)生成自己的類型聲明文件供主模塊加載)
使用 Project References 改造示例項(xiàng)目
根據(jù)目前我們對Project References的認(rèn)識,現(xiàn)在可以開始改造一下我們的項(xiàng)目了,首先是根目錄下的tsconfig.json配置,它起到一個類似于項(xiàng)目入口的作用,因此這里面只負(fù)責(zé)添加references聲明項(xiàng)目中需要被構(gòu)建的模塊,以及通過exclude將不需要參與構(gòu)建的模塊排除(比如src/__test__中的測試代碼)
/tsconfig.json
{
"references": [
{ "path": "src/client" },
{ "path": "src/server" },
{ "path": "src/shared" }
],
"exclude": ["**/__test__"]
}
然后是各個子模塊的tsconfig.json配置,這里我們假設(shè)構(gòu)建目標(biāo)為es5的代碼,所以對于client、server以及shared來說是存在公共配置的,所以我們可以抽離出一個公共配置,然后在子模塊中通過extends配置項(xiàng)公用一個配置
/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
}
}
全量構(gòu)建
現(xiàn)在我們在項(xiàng)目根目錄下運(yùn)行tsc --build --verbose,就會根據(jù)references配置去尋找各個子模塊,并對它們進(jìn)行構(gòu)建,可以理解為對項(xiàng)目的全量構(gòu)建
--build 參數(shù)表示讓tsc以build模式進(jìn)行構(gòu)建和類型檢查,也就是會使用references配置項(xiàng),如果不開啟的話是不會使用references配置項(xiàng)的,這點(diǎn)可以從官方文檔中得證:

--verbose 參數(shù)則是會將構(gòu)建過程中的輸出顯示在控制臺中,不開啟該參數(shù)的話則不會顯示輸出(除非構(gòu)建過程中報(bào)錯)
運(yùn)行后/dist目錄結(jié)構(gòu)如下
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
可以看到,所有子模塊都被構(gòu)建進(jìn)來了,各模塊的產(chǎn)物中有一個tsconfig.tsbuildinfo文件,這個文件起到一個類似緩存的作用,對于后續(xù)進(jìn)行增量構(gòu)建有著重要作用
前面看到的官方文檔中對tsc --build的作用的介紹中的第二點(diǎn)Detect if they are up-to-date主要就是依靠這個緩存文件去識別的
開啟--verbose參數(shù)后,可以看到控制臺輸出如下:
[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意思就是這個模塊沒被構(gòu)建過,因此會開始對其進(jìn)行構(gòu)建
由于我們是首次構(gòu)建,所以三個模塊都是沒被構(gòu)建過的,所以三個模塊都被檢測為out of date
當(dāng)我們再次運(yùn)行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,從而避免了重復(fù)構(gòu)建
增量構(gòu)建
如果現(xiàn)在我們修改了client模塊的代碼,再運(yùn)行tsc --build --verbose會怎樣呢?估計(jì)你也能猜到了,只有client模塊會被構(gòu)建,而其他模塊則會跳過
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'...
相信現(xiàn)在你能體會到Project References的好處了吧,能夠很大程度上優(yōu)化我們的構(gòu)建速度!
不過實(shí)際開發(fā)中,tsc更多的是用來進(jìn)行類型檢查,至于compile的工作,則更多地是交給如Babel、swc、esbuild等工具去完成,這也是官方文檔中有提到過的

這也是為什么你在vite創(chuàng)建的項(xiàng)目中能夠看到默認(rèn)的build命令配置為tsc && vite build,正是將類型檢查的工作交給tsc,而構(gòu)建工作則交給vite底層依賴的rollup去完成
對__test__測試代碼的處理
我們的改造貌似已經(jīng)完成了,但其實(shí)還忽略了一個src/__test__,它也可以被視為一個模塊,它作為主模塊,依賴了client和server,因此也可以給它加上tsconfig.json配置,并且對于測試代碼,我們一般不希望將它們構(gòu)建成js,只希望tsc負(fù)責(zé)類型檢查的工作,因此我們需要進(jìn)行如下配置:
src/__test__/tsconfig.json
{
"compilerOptions": {
"noEmit": true
},
"references": [{ "path": "../client" }, { "path": "../server" }]
}
noEmit的作用剛剛在官方文檔中也看到了,不會把產(chǎn)物文件輸出,如果我們只需要類型檢查能力的話很適合開啟該配置項(xiàng)
現(xiàn)在我們?nèi)绻枰獙?code>__test__中的代碼進(jìn)行類型檢查的話,只需要執(zhí)行:
# 忽略 references 配置項(xiàng) tsc --project src/__test__ # 啟用 references 配置項(xiàng) tsc --build src/__test__
如果是使用--project參數(shù)的話,tsconfig.json中可以忽略references配置項(xiàng),因?yàn)榧幢闩渲昧艘膊粫皇褂茫@在依賴產(chǎn)物未構(gòu)建出來時能起作用
而如果使用--build參數(shù),并且client和server未構(gòu)建出來時,會先構(gòu)建它們,再對測試代碼進(jìn)行類型檢查,可以根據(jù)個人需求場景來決定使用--project還是--build
總結(jié)
本篇文章介紹了Project References是什么,并通過一個簡單的示例項(xiàng)目,并結(jié)合TypeScript Documentation官方文檔邊實(shí)戰(zhàn)邊解釋
總的來說,其使用起來就是:
- 主模塊(
tsc --build作用的模塊視為主模塊)中通過references配置項(xiàng)聲明依賴的模塊 - 被引用模塊中開啟
composite和declaration配置項(xiàng)以支持被引用 - 通過
tsc --build 主模塊才可以啟用references配置項(xiàng),這在官方文檔中被稱為Build Mode,如果直接tsc 主模塊的話,是不會啟用references配置項(xiàng)的,也就導(dǎo)致依然會對項(xiàng)目中的所有ts文件進(jìn)行編譯(如果沒配置include或files配置項(xiàng)的話)
希望通過本篇文章,能夠讓你對Project References有一個全面了解,也希望能夠?qū)⑵溆迷谀愕捻?xiàng)目中,提升類型檢查或構(gòu)建(使用 tsc 進(jìn)行構(gòu)建的話)的速度,更多關(guān)于tsc性能Project References的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
紅黑樹的插入詳解及Javascript實(shí)現(xiàn)方法示例
這篇文章主要給大家介紹了關(guān)于紅黑樹的插入的相關(guān)資料,以及Javascript實(shí)現(xiàn)的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起看看吧。2018-03-03
javascript 最常用的10個自定義函數(shù)[推薦]
如果不使用類庫或者沒有自己的類庫,儲備一些常用函數(shù)總是有好處的。2009-12-12
js當(dāng)前頁面登錄注冊框,固定div,底層陰影的實(shí)例代碼
下面小編就為大家?guī)硪黄猨s當(dāng)前頁面登錄注冊框,固定div,底層陰影的實(shí)例代碼。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-10-10
JS函數(shù)內(nèi)部屬性之a(chǎn)rguments和this實(shí)例解析
在函數(shù)內(nèi)部,有兩個特殊的對象:arguments和this。這篇文章主要介紹了函數(shù)內(nèi)部屬性之a(chǎn)rguments和this ,需要的朋友可以參考下2018-10-10
基于javascript實(shí)現(xiàn)圖片懶加載
這篇文章主要介紹了javascript實(shí)現(xiàn)圖片懶加載的方法及思路,有時我們需要用懶加載,也就是延遲加載圖片的方式,來提高網(wǎng)站的親和力,需要的朋友可以參考下2016-01-01

