TypeScript 編譯與設定 ── Incremental Builds(增量編譯)
簡介
在大型前端或 Node.js 專案中,TypeScript 的編譯時間往往會成為開發者的瓶頸。每次執行 tsc 時,編譯器都會重新掃描整個程式碼庫,產生大量的檔案與型別檢查,即使實際變動的檔案只佔專案的一小部分。
為了提升開發效率,TypeScript 從 3.4 版開始支援 Incremental Builds(增量編譯)。透過此功能,編譯器會記錄先前的編譯結果,下次執行時只重新編譯受影響的檔案,顯著縮短編譯時間。
本篇文章將深入說明 incremental builds 的原理、設定方式、實務範例以及易踩的陷阱,幫助從初學者到中階開發者在日常開發中善用此功能。
核心概念
1. 為什麼需要增量編譯?
- 開發迭代快:只編譯改動的檔案,省下大量 I/O 與型別檢查時間。
- CI/CD 效率提升:在持續整合環境中,增量編譯可以讓測試與部署更快完成。
- 資源節省:減少 CPU 與記憶體佔用,對於資源受限的 CI runner 特別有幫助。
2. incremental 與 composite 的差別
| 選項 | 目的 | 產出檔案 | 常見使用情境 |
|---|---|---|---|
incremental: true |
記錄上一次編譯的狀態,僅在單一專案中使用 | .tsbuildinfo(二進位快取檔) |
開發階段的快速迭代 |
composite: true |
建立可被其他專案引用的「可組合」專案 | .tsbuildinfo + *.d.ts + *.js |
多個子模組(project references)組成的大型 monorepo |
重點:
composite內部已隱含incremental,但incremental本身不會產生.d.ts,僅適用於單一專案。
3. .tsbuildinfo 檔案的運作原理
- 編譯器在第一次執行時,會把每個檔案的 語法樹(AST)、型別檢查結果 與 輸出檔案的時間戳 寫入
.tsbuildinfo。 - 下次執行時,
tsc會比較檔案的 修改時間 與 快取中的時間戳,若相同則直接跳過該檔案的重新編譯。 - 若有相依檔案變動,編譯器會自動向上追溯,重新編譯受影響的檔案。
4. 基本設定範例
在 tsconfig.json 中加入以下屬性即可啟用增量編譯:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"strict": true,
"incremental": true, // ★ 啟用增量編譯
"tsBuildInfoFile": "./.tsbuildinfo", // 可自行指定快取檔位置
"outDir": "./dist"
},
"include": ["src/**/*.ts"]
}
小技巧:將
.tsbuildinfo加入.gitignore,避免快取檔被提交至版本庫。
程式碼範例
以下示範三個常見情境,說明增量編譯的實際行為與最佳寫法。
範例 1:基本增量編譯流程
# 第一次完整編譯,會產生 .tsbuildinfo
$ tsc
# 修改 src/util.ts
$ echo "// 新增函式" >> src/util.ts
# 再次編譯,只會重新編譯 util.ts 以及相依的檔案
$ tsc
說明:
tsc只會重新編譯util.ts(以及 import 它的檔案),其他未變動的檔案直接使用快取結果。
範例 2:使用 composite 與 Project References
// packages/core/tsconfig.json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"outDir": "../../dist/core"
},
"include": ["src"]
}
// packages/app/tsconfig.json
{
"compilerOptions": {
"outDir": "../../dist/app"
},
"references": [
{ "path": "../core" } // 依賴 core 專案
],
"include": ["src"]
}
# 先編譯 core(產生 .tsbuildinfo)
$ tsc -b packages/core
# 再編譯 app,若 core 沒變動,app 的編譯會直接使用 core 的快取
$ tsc -b packages/app
說明:
-b(build mode)會自動偵測子專案的增量狀態,適合 monorepo 結構。
範例 3:自訂 tsBuildInfoFile 位置與清除快取
{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": "temp/cache/tsbuildinfo.json"
}
}
# 編譯產生自訂位置的快取檔
$ tsc
# 若快取檔損毀或想重新做乾淨編譯
$ rm -rf temp/cache/tsbuildinfo.json
$ tsc # 會回到完整編譯的行為
提醒:在 CI 環境中,建議在每次執行前先清除舊的
.tsbuildinfo,避免「舊快取」導致型別不一致的錯誤。
範例 4:結合 watch 模式的增量編譯
# 開啟 watch 模式,編譯器會持續追蹤檔案變動
$ tsc -w
# 修改任意檔案,編譯器僅重新編譯受影響的檔案
# 此時仍會使用 .tsbuildinfo 作為快取來源
優點:
-w本身即使用增量快取,配合incremental可以讓開發者感受到毫秒級的回饋。
範例 5:在 webpack 或 esbuild 中使用增量快取
// webpack.config.js
module.exports = {
// ...
plugins: [
new ForkTsCheckerWebpackPlugin({
async: false,
typescript: {
// 直接使用 tsconfig 中的 incremental 設定
configFile: 'tsconfig.json'
}
})
]
};
// esbuild.config.mjs
import { build } from 'esbuild';
await build({
entryPoints: ['src/index.ts'],
outdir: 'dist',
bundle: true,
tsconfig: 'tsconfig.json', // esbuild 會自動讀取 incremental 設定
incremental: true // 讓 esbuild 本身也支援增量
});
說明:即使在打包工具中,仍可藉由
incremental快取提升重建速度;但要注意每個工具的快取策略可能不同,仍建議保留.tsbuildinfo供 TypeScript 本身使用。
常見陷阱與最佳實踐
| 陷阱 | 可能的症狀 | 解決方式 / 最佳實踐 |
|---|---|---|
| 快取檔遺失或被誤刪 | 下一次編譯回到全量編譯,耗時變長 | 將 .tsbuildinfo 加入 .gitignore,並在 CI 中確保每次執行前都產生或清除它。 |
| 相依檔案的改動未被偵測 | 編譯結果與實際程式碼不一致,導致執行時錯誤 | 確保 include / exclude 正確,且所有 import 的檔案都在編譯範圍內。 |
使用 composite 卻未輸出 .d.ts |
其他子專案找不到型別定義 | 在 tsconfig.json 中加入 "declaration": true,或在根專案使用 references。 |
| 快取檔與 TypeScript 版本不匹配 | tsc 報錯 Cannot read property 'version' of undefined |
每次升級 TypeScript 後,刪除舊的 .tsbuildinfo 重新編譯。 |
| 過度依賴增量快取,忽視型別正確性 | 在大型重構後,舊快取導致錯誤隱藏 | 定期執行一次乾淨編譯(tsc --build --force)以驗證整體型別一致性。 |
最佳實踐清單
- 將
.tsbuildinfo加入.gitignore,避免快取檔污染版本庫。 - 在 CI/CD pipeline 中使用
tsc -b --force先清除舊快取,再執行增量編譯,以保證產出一致。 - 結合
watch與incremental,在本機開發時獲得即時回饋。 - 使用
composite+references,在 monorepo 中建立明確的模組邊界,讓每個子專案都能獨立增量編譯。 - 定期檢查
tsconfig.json的include/exclude,確保新加入的檔案不會被意外排除。
實際應用場景
1. 大型前端單頁應用(SPA)
- 情境:React + Redux + TypeScript,程式碼量超過 200,000 行。
- 做法:在
tsconfig.json中開啟incremental,配合webpack --watch或vite的 HMR。開發者每次保存檔案,編譯時間從 5 秒降至約 0.3 秒,提升開發體驗。
2. Node.js 微服務集群
- 情境:每個服務都是獨立的 TypeScript 專案,使用
npm workspaces管理。 - 做法:為每個服務設定
composite: true,再在根目錄使用references。CI 中僅重新編譯受變更服務,其他服務直接使用快取,大幅縮短 CI 時間。
3. 多平台共用程式庫(Library)
- 情境:同時產出
ESM、CommonJS、d.ts三種格式的套件。 - 做法:在
tsconfig.build.json中啟用incremental,並在package.json的prepublishOnly內執行tsc -b. 只要程式碼有改動,就只重新產出變更的檔案,發佈流程更快。
4. 教學或範例專案
- 情境:教學網站提供即時編譯的 TypeScript 範例。
- 做法:在後端使用
esbuild --incremental搭配 TypeScript 的.tsbuildinfo,讓每次使用者提交程式碼時只重新編譯差異部份,降低伺服器負載。
總結
Incremental builds 是 TypeScript 為提升開發與 CI 效率所提供的關鍵功能。透過 incremental 或 composite 設定,編譯器會將先前的編譯結果快取至 .tsbuildinfo,在後續編譯時僅處理變動的檔案,從而縮短編譯時間、降低資源消耗。
在實務上,我們建議:
- 在所有專案的
tsconfig.json中開啟incremental(或在多模組環境使用composite+references)。 - 將快取檔納入
.gitignore,避免版本衝突。 - 結合
watch、webpack、esbuild等工具,讓增量編譯的效益最大化。 - 定期執行乾淨編譯,確保型別正確性不受舊快取影響。
掌握上述概念與實踐方法,你將在大型 TypeScript 專案中體驗到 秒級回饋,讓開發流程更順暢、部署更快速。祝你寫程式愉快,編譯永遠不再是瓶頸!