# tsrs vs tsgo：Cloudflare 的“8 倍”怎么读？我把两个编译器放一起跑了四个样例

> Canonical: https://vxi.me/posts/tsrs-vs-tsgo-typescript-rust-compiler/
> Locale: zh-cn
> Published: 2026-10-22
> Updated: 2026-10-22
> Markdown: https://vxi.me/posts/tsrs-vs-tsgo-typescript-rust-compiler.md

<figure class="tsrs-parity-hero not-prose">
<picture><source media="(prefers-reduced-motion: reduce) and (max-width: 640px)" srcset="/images/posts/tsrs-vs-tsgo-typescript-rust-compiler/tsrs-diagnostic-parity.mobile.still.zh-cn.svg" type="image/svg+xml"><source media="(prefers-reduced-motion: reduce)" srcset="/images/posts/tsrs-vs-tsgo-typescript-rust-compiler/tsrs-diagnostic-parity.still.zh-cn.svg" type="image/svg+xml"><source media="(max-width: 640px)" srcset="/images/posts/tsrs-vs-tsgo-typescript-rust-compiler/tsrs-diagnostic-parity.mobile.zh-cn.svg" type="image/svg+xml"><img src="/images/posts/tsrs-vs-tsgo-typescript-rust-compiler/tsrs-diagnostic-parity.zh-cn.svg" alt="tsgo 与 tsrs 使用匹配的 TypeScript 源版本运行四组 TypeScript 合成样例，比较诊断和退出码；四组结果一致。" width="1200" height="700" loading="eager" fetchpriority="high" decoding="async" style="display:block;width:100%;height:auto"></picture>
<figcaption>图：tsgo 与 tsrs 在四组单文件合成样例中的诊断对照。四组输出一致；这不是项目级兼容性或性能基准。</figcaption>
</figure>

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 的[公告](https://blog.cloudflare.com/typescript-rust-port/)把早期结果概括为最高快 8 倍、内存约少 70%。固定提交的 tsrs [历史基准报告](https://github.com/oxc-project/tsrs/blob/357d1d3ec23180eae94a8485b8b7444487a7ea5f/bench/RESULTS.md)里，列出的倍率是这样的：

| 上游历史基准 | 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 个；但把 checker 数对齐后，表里的倍率仍然超过 8 倍。这里有个容易忽略的区别：Cloudflare 公告讲的是早期基准的概括，仓库里的 `RESULTS.md` 则是另一份固定版本、固定环境的历史快照。不能把前者当成这张表的上限，也不能把后者的最高倍率当成自己的项目一定能拿到的成绩。

为什么差这么多？我不想盯着表格猜一个漂亮的原因。能确认的是，这份快照跑在 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 源码提交：[b85298b6a81f772d080b0455de0ca9d744cd6fd6](https://github.com/microsoft/TypeScript/commit/b85298b6a81f772d080b0455de0ca9d744cd6fd6)。

装依赖时先踩了个很具体的坑：我把 TypeScript 源码仓库里的 `@typescript/typescript` workspace 名称当成 npm 上能直接安装的包，结果拿到 E404，编译器根本没启动。公开发布的根包叫 `typescript`；这个版本的命令行入口仍是 `tsc`，Linux x64 的原生 Go 编译器由 `@typescript/typescript-linux-x64` 提供。把包名改对，才跑到真正的编译步骤。

四个夹具共用同一份 `tsconfig.json`。我把 `types` 显式设成空数组，避免测试环境自动带入全局 `@types` 包；`skipLibCheck: false` 则让声明文件检查保持开启。这样至少能把这轮对照实际用的配置摊出来，而不是只给一个“严格模式”的口头描述。

~~~json
{
  "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 固定版本的[兼容性说明](https://github.com/oxc-project/tsrs/blob/357d1d3ec23180eae94a8485b8b7444487a7ea5f/docs/COMPATIBILITY.md)里，错误基线对上了 13,458 / 13,462 项，`.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，没测试这些平台、IDE 插件或 LSP，所以如果你的日常工作流依赖 `astro check` 或编辑器实时诊断，先对照[这个版本的兼容性表](https://github.com/oxc-project/tsrs/blob/357d1d3ec23180eae94a8485b8b7444487a7ea5f/docs/COMPATIBILITY.md)再决定要不要折腾。（如果你正好在排查 Astro 升级后的类型检查问题，我之前也整理过一篇[兼容排查笔记](/posts/astro-check-typescript-7-compatibility/)。）

## 在自己的项目里对照，什么结果才够用

建议先在一次性 worktree 或独立分支里试，别直接改日常工作区。这一步就是为了试完能随时丢掉，不影响手头的项目。

下面这段脚本防的是一种很容易漏掉的情况：两边退出码一样，但具体诊断不一样。这里的 `set +e` 不是笔误——有错误的夹具本来就会让编译器返回非零；先让两边都跑完并记下退出码，才能继续比较输出。脚本里的 `tsc` 是固定 TypeScript 7.1-dev 包提供的 Go 编译器入口，`/tmp/tsgo.out` 只是日志文件名。

~~~bash
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 优化）。不要一开始就打开这些开关，不然你会看不到默认行为到底差在哪。具体参数见[使用说明](https://github.com/oxc-project/tsrs/blob/357d1d3ec23180eae94a8485b8b7444487a7ea5f/docs/USAGE.md)和[默认值差异表](https://github.com/oxc-project/tsrs/blob/357d1d3ec23180eae94a8485b8b7444487a7ea5f/docs/DEFAULTS.md)。

上面只对诊断，不测耗时和内存。我自己这次没把性能测量跑完；如果你想先摸一下手头项目，可以用下面的命令记一次运行时间和峰值 RSS。Linux 用 GNU `time`；macOS 先装 `gnu-time`，再把第一行改成 `TIME_BIN=gtime`。

~~~bash
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 轮，锁住依赖、版本、`tsconfig` 和 checker 数，再看中位耗时和峰值 RSS。冷启动和热缓存也分开记；我没去清理操作系统 page cache 那一层，所以不会把它说成完整的系统级冷缓存测试。GNU `time` 的 `%M` 通常按 KiB 记峰值 RSS，跨平台比较前先确认单位。

我会先让 tsrs 在真实项目的 CI 里当一条额外任务陪跑，原有的 `tsc` 和框架检查继续做必需门禁。等诊断差异解释清楚、性能测量有了稳定结果，再让它跟完一轮真实 PR 和发布周期，并确认回滚路径能走通，才值得讨论要不要替换。四个小样例让我确认的只是起点：在这几种输入上，两边给出了相同诊断。

实测日期：2026 年 10 月 22 日
