tsrs vs tsgo:Cloudflare 宣稱快 8 倍,為何基準表卻有 16.87 倍?
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 固定提交的歷史基準報告,逐項列出的結果如下:
| 上游歷史測試 | tsgo | tsrs | checker 數 | 上游倍率 |
|---|---|---|---|---|
| mui-docs, 預設 | 12.66 秒 | 0.75 秒 | 4 vs 16 | 16.87 倍 |
mui-docs, 雙方 --checkers 4 | 12.69 秒 | 1.07 秒 | 4 vs 4 | 11.81 倍 |
mui-docs, 雙方 --checkers 16 | 12.36 秒 | 0.77 秒 | 16 vs 16 | 16.07 倍 |
| vscode, 預設 | 11.10 秒 | 0.82 秒 | 4 vs 16 | 13.55 倍 |
vscode, 雙方 --checkers 16 | 6.73 秒 | 0.81 秒 | 16 vs 16 | 8.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 實測結果 |
|---|---|---|
valid | export const answer: number = 42; | 0 個診斷,結束碼 0/0 |
type-error | export const answer: number = "wrong"; | TS2322、結束碼 2/2 |
module-missing | import {missing} from "./unavailable.js"; export const answer = missing; | TS2307、結束碼 2/2 |
generic-constraint | type 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 日