本文 AI 產出,尚未審核

TypeScript 編譯與設定 ── Incremental Builds(增量編譯)


簡介

在大型前端或 Node.js 專案中,TypeScript 的編譯時間往往會成為開發者的瓶頸。每次執行 tsc 時,編譯器都會重新掃描整個程式碼庫,產生大量的檔案與型別檢查,即使實際變動的檔案只佔專案的一小部分。
為了提升開發效率,TypeScript 從 3.4 版開始支援 Incremental Builds(增量編譯)。透過此功能,編譯器會記錄先前的編譯結果,下次執行時只重新編譯受影響的檔案,顯著縮短編譯時間。

本篇文章將深入說明 incremental builds 的原理、設定方式、實務範例以及易踩的陷阱,幫助從初學者到中階開發者在日常開發中善用此功能。


核心概念

1. 為什麼需要增量編譯?

  • 開發迭代快:只編譯改動的檔案,省下大量 I/O 與型別檢查時間。
  • CI/CD 效率提升:在持續整合環境中,增量編譯可以讓測試與部署更快完成。
  • 資源節省:減少 CPU 與記憶體佔用,對於資源受限的 CI runner 特別有幫助。

2. incrementalcomposite 的差別

選項 目的 產出檔案 常見使用情境
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:在 webpackesbuild 中使用增量快取

// 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)以驗證整體型別一致性。

最佳實踐清單

  1. .tsbuildinfo 加入 .gitignore,避免快取檔污染版本庫。
  2. 在 CI/CD pipeline 中使用 tsc -b --force 先清除舊快取,再執行增量編譯,以保證產出一致。
  3. 結合 watchincremental,在本機開發時獲得即時回饋。
  4. 使用 composite + references,在 monorepo 中建立明確的模組邊界,讓每個子專案都能獨立增量編譯。
  5. 定期檢查 tsconfig.jsoninclude/exclude,確保新加入的檔案不會被意外排除。

實際應用場景

1. 大型前端單頁應用(SPA)

  • 情境:React + Redux + TypeScript,程式碼量超過 200,000 行。
  • 做法:在 tsconfig.json 中開啟 incremental,配合 webpack --watchvite 的 HMR。開發者每次保存檔案,編譯時間從 5 秒降至約 0.3 秒,提升開發體驗。

2. Node.js 微服務集群

  • 情境:每個服務都是獨立的 TypeScript 專案,使用 npm workspaces 管理。
  • 做法:為每個服務設定 composite: true,再在根目錄使用 references。CI 中僅重新編譯受變更服務,其他服務直接使用快取,大幅縮短 CI 時間。

3. 多平台共用程式庫(Library)

  • 情境:同時產出 ESMCommonJSd.ts 三種格式的套件。
  • 做法:在 tsconfig.build.json 中啟用 incremental,並在 package.jsonprepublishOnly 內執行 tsc -b. 只要程式碼有改動,就只重新產出變更的檔案,發佈流程更快。

4. 教學或範例專案

  • 情境:教學網站提供即時編譯的 TypeScript 範例。
  • 做法:在後端使用 esbuild --incremental 搭配 TypeScript 的 .tsbuildinfo,讓每次使用者提交程式碼時只重新編譯差異部份,降低伺服器負載。

總結

Incremental builds 是 TypeScript 為提升開發與 CI 效率所提供的關鍵功能。透過 incrementalcomposite 設定,編譯器會將先前的編譯結果快取至 .tsbuildinfo,在後續編譯時僅處理變動的檔案,從而縮短編譯時間、降低資源消耗。

在實務上,我們建議:

  1. 在所有專案的 tsconfig.json 中開啟 incremental(或在多模組環境使用 composite + references)。
  2. 將快取檔納入 .gitignore,避免版本衝突。
  3. 結合 watchwebpackesbuild 等工具,讓增量編譯的效益最大化。
  4. 定期執行乾淨編譯,確保型別正確性不受舊快取影響。

掌握上述概念與實踐方法,你將在大型 TypeScript 專案中體驗到 秒級回饋,讓開發流程更順暢、部署更快速。祝你寫程式愉快,編譯永遠不再是瓶頸!