tsgo 與 tsrs 使用相同 TypeScript 原始碼版本執行四組合成樣例,比對診斷與結束碼;四組結果一致。
圖:tsgo 與 tsrs 在四組單檔合成樣例中的診斷比對。四組輸出一致;這不是專案級相容性或效能基準。

Cloudflare 介紹 tsrs 時說,相較 TypeScript 7.0,最高快 8 倍、記憶體約少 70%。我翻到 tsrs 固定提交的歷史基準表,卻看見 mui-docs 有 16.87 倍,vscode 預設設定有 13.55 倍。當下最想知道的是:這跟公告中的 8 倍到底怎麼對上?

我沒有重新執行那套大型專案效能基準。這次換個小一點、可以查驗的問題:把 tsrs 和 tsgo(TypeScript 的 Go 實作)固定在對應版本,放到 GitHub 託管的暫時隔離環境,用四個 TypeScript 小範例比對診斷。四組都一致,讓我覺得值得繼續測;但離替換正式 CI 的型別檢查工具仍有一段距離。

讀官方跑分,為什麼要先看 checker 數?

Cloudflare 的公告把早期結果概括為最高快 8 倍、記憶體約少 70%;tsrs 固定提交的歷史基準報告,逐項列出的結果如下:

上游歷史測試tsgotsrschecker 數上游倍率
mui-docs, 預設12.66 秒0.75 秒4 vs 1616.87 倍
mui-docs, 雙方 --checkers 412.69 秒1.07 秒4 vs 411.81 倍
mui-docs, 雙方 --checkers 1612.36 秒0.77 秒16 vs 1616.07 倍
vscode, 預設11.10 秒0.82 秒4 vs 1613.55 倍
vscode, 雙方 --checkers 166.73 秒0.81 秒16 vs 168.32 倍

先看預設設定:tsgo 用 4 個 checker,tsrs 卻用 16 個,並不是相同條件。但就算挑雙方都用 4 個或 16 個 checker 的資料列,倍率依舊超過 8 倍。差別在於公告描述的是早期測試的概括值,RESULTS.md 是另一份固定版本和測試環境的歷史快照。不能把公告的 8 倍當成這份表格的上限,更不能把表格的 16 倍當成每個專案都能實現的成績。

為什麼差距這麼大?我不想光看表格就猜一個看似合理的原因。能確定的是,報告使用 Depot CI 的 depot-ubuntu-24.04-16(16 vCPU、63 GB RAM、Linux x86_64),時間和峰值記憶體採交錯執行的中位數;tsgo 基準是 typescript@7.0.2,tsrs 移植的則是較新的 TypeScript 7.1-dev,並使用 PGO 最佳化的發行版本。即使 checker 數相同,原始碼版本和編譯方式也未完全對齊。

記憶體也一樣依專案與設定變動。mui-docs 預設設定下報告的是記憶體減少 4.61 倍;雙方都設為 16 個 checker 時則是減少 8.23 倍。公告的「約少 70%」只能理解成早期結果的概括,不能當作每個專案的保證。我這次查核的是上游報告,並沒有重跑大型專案基準。

四組診斷測試到底涵蓋了什麼

我固定使用 tsrs@0.18.0-ts7.1.0-dev.20260929,對照 typescript@7.1.0-dev.20260930.4 的 Go 實作。兩者對應同一個 TypeScript 原始碼提交 b85298b6。

安裝相依套件時先踩了個坑:我把 TypeScript 原始碼倉庫的 @typescript/typescript workspace 名稱,當成 npm 上可以直接安裝的套件,結果得到 E404,編譯器根本沒啟動。公開發行的根套件名稱是 typescript;在這個 nightly 版本裡,CLI 仍叫 tsc,Linux x64 的原生 Go 編譯器則由 @typescript/typescript-linux-x64 提供。修正名稱後才真正開始編譯。

四組測試共用同一份 tsconfig.json。我明確設置 types: [],避免環境中全域安裝的 @types 自動混進測試;skipLibCheck: false 則保留宣告檔的型別檢查。以下是這次實際使用的設定,不只是一句「嚴格模式」:

{
  "compilerOptions": {
    "strict": true,
    "target": "es2022",
    "module": "esnext",
    "noEmit": true,
    "skipLibCheck": false,
    "types": []
  },
  "include": ["src/*.ts"]
}

每組測試目錄只放一個 src/test.ts,完整內容如下:

測試目錄src/test.ts 完整內容tsgo/tsrs 實測結果
validexport const answer: number = 42;0 個診斷,結束碼 0/0
type-errorexport const answer: number = "wrong";TS2322、結束碼 2/2
module-missingimport {missing} from "./unavailable.js"; export const answer = missing;TS2307、結束碼 2/2
generic-constrainttype OnlyText<T extends string> = {value:T}; export type Bad = OnlyText<number>;TS2344、結束碼 2/2

雙方都帶入 --noEmit --pretty false --checkers 4。我逐一比較檔案、行列位置、錯誤碼、訊息、順序與程序結束碼,四組都相符。不過每組只有一個來源檔,--checkers 4 在這裡主要是固定測試參數,沒有真的壓測四路平行處理。

錯誤總數一樣,為什麼還不夠?

只看錯誤數量,很容易高興得太早。tsrs 固定版本的相容性說明中,13,462 項錯誤基準有 13,458 項相符;.types 和 .symbols 的 12,779 項基準則全部相符。

drizzle-orm 就是現成的反例。上游報告記載,雙方都用 16 個 checker 時,各自回報 10,846 個錯誤,但診斷位置不同,並被上游標示為 tsrs bug。我並未親自重現這個案例,而是根據固定版本的報告。對 CI 和程式碼審查來說,錯誤指向哪個檔案、哪一行,比總數是否好看更重要。

相容性表還有幾個實際限制:這個版本尚不支援 --init、--showConfig;--locale 無法翻譯診斷訊息;.astro、.vue、.svelte 的 content mapper 可用於 CLI/build,但不支援語言伺服器。預先編譯的套件也沒有涵蓋 Windows 和 Intel macOS。我這次只在 GitHub 隔離 Linux 環境執行 CLI,沒測這些平台、編輯器外掛或 LSP。若工作流程依賴 astro check 或編輯器即時診斷,請先確認對應版本的相容性表。若你也碰到 Astro 升級後的型別檢查問題,我另有一篇相依套件相容性排查筆記。

想在自己的專案裡試,我會先比什麼

我會先開一次性 worktree 或獨立分支,不直接碰日常工作目錄。試完可以整份丟掉,不必擔心影響正在開發的專案。

以下腳本要防的是:兩邊結束碼相同,診斷內容卻不一樣。set +e 不是打錯字;刻意寫錯的測試本來就會回傳非零,必須先讓兩個編譯器都跑完、記下狀態,再比對輸出。此處的 tsc 是固定 TypeScript nightly 的 Go 編譯器入口,/tmp/tsgo.out 只是輸出檔名。

pnpm add -D --save-exact tsrs@0.18.0-ts7.1.0-dev.20260929 typescript@7.1.0-dev.20260930.4

set +e
pnpm exec tsc -p . --noEmit --pretty false --checkers 4 > /tmp/tsgo.out 2>&1
tsgo_status=$?
pnpm exec tsrs -p . --noEmit --pretty false --checkers 4 > /tmp/tsrs.out 2>&1
tsrs_status=$?
diff -u /tmp/tsgo.out /tmp/tsrs.out
diff_status=$?
set -e
printf 'tsgo_exit=%s tsrs_exit=%s diff_exit=%s\n' "$tsgo_status" "$tsrs_status" "$diff_status"
if [ "$tsgo_status" -ne "$tsrs_status" ] || [ "$diff_status" -ne 0 ]; then
  echo 'DIAGNOSTIC_DIFF_OR_EXIT_MISMATCH: inspect /tmp/tsgo.out and /tmp/tsrs.out'
  exit 1
fi

如果第一次就有差異,先保留原始輸出,再個別嘗試 --checkerAssignment go(使用 tsgo 的檔案分配與 checker 歷史)或 --noLazyMembers(關閉 tsrs 的 lazy-member 最佳化)。不要在首次對照前就打開,否則可能看不到預設行為的差異。參數可參考使用說明和預設差異文件。

上面的腳本只比診斷,不測時間或記憶體。我這次並未完成整套效能測量;如果要先摸清自己專案的情況,Linux 可用 GNU time 記錄一次執行時間與峰值 RSS。macOS 安裝 gnu-time 後,將第一行改成 TIME_BIN=gtime。

TIME_BIN=/usr/bin/time

set +e
"$TIME_BIN" -f 'elapsed_s=%e max_rss=%M' -o /tmp/tsgo-bench.metrics \
  pnpm exec tsc -p . --noEmit --pretty false --checkers 4 \
  > /tmp/tsgo-bench.out 2>&1
tsgo_status=$?
"$TIME_BIN" -f 'elapsed_s=%e max_rss=%M' -o /tmp/tsrs-bench.metrics \
  pnpm exec tsrs -p . --noEmit --pretty false --checkers 4 \
  > /tmp/tsrs-bench.out 2>&1
tsrs_status=$?
set -e

printf 'tsgo_exit=%s ' "$tsgo_status"; cat /tmp/tsgo-bench.metrics
printf 'tsrs_exit=%s ' "$tsrs_status"; cat /tmp/tsrs-bench.metrics

這只是一對結果,背景負載稍有變化就可能產生噪聲。真的要比較,我會在同一台 runner 上交錯進行至少 5 輪成對測量,固定 lockfile、版本、tsconfig 與 checker 數,最後看中位耗時和峰值 RSS。冷啟動與熱快取要分別記錄;我沒有清理作業系統的 page cache,所以不會稱之為完整的系統層冷快取測試。GNU time 的 %M 通常以 KiB 表示峰值 RSS,跨平台比較前要先確認單位。

我會先讓 tsrs 在真正的專案 CI 裡以額外、不阻擋合併的工作陪跑,原本的 tsc 和框架檢查仍保留為必要門檻。等診斷差異能解釋、效能測量穩定,並經過一次完整 PR/發行週期及回復方案確認後,再討論是否替換。這四個小範例目前只證明一件事:在這幾類輸入上,兩個編譯器給出了相同的診斷。

實測日期:2026 年 10 月 22 日