tsrs:用 Rust 重写 TypeScript,快 8 倍、内存少约 70%

·阅读约12分钟·Evergreen Tools Team

类型检查器是大多数 JavaScript 项目里最慢的一步。改一行代码,等四十秒,这四十秒你既不在写代码也不在想架构。2026 年 10 月,Cloudflare 与 VoidZero 联合 Owner.com 的 Max Schwenk 公布了 tsrs——一个用 Rust 重写的 TypeScript 编译器,孵化在 Oxc 工具链里,并与 TypeScript 团队密切合作。早期基准:在相同类型检查 worker 数量下,它比 TypeScript 7.0 快最多 8 倍、内存占用少约 70%。这篇文章不吹这个数字,讲三件事:它不是 Fork 意味着什么、怎么用你自己的仓库量出真实差距、以及在你把它放进 CI 之前需要跑什么对照。

深色屏幕上的源代码

类型检查器通常是 JavaScript CI 里最慢的一环

一、先纠正一个误解:它不是 Fork

Cloudflare 明确写了三遍:我们不是在 Fork TypeScript。语言怎么设计、往哪走,仍然由 TypeScript 团队说了算。这个姿态不是礼貌,是工程必需。TypeScript 团队的很多特性会花上几年反复试原型才定下来,一个 Rust 移植必须紧跟着这些决定走,否则迟早会偏离、失去兼容性。所以 tsrs 的定位是「忠实移植」——严格按原始实现复刻语义,而不是「我顺手优化一下」。这条边界对使用者的实际意义是:你可以把它当成一个更快的实现来评估,但不能指望它提供 TypeScript 没有的新能力。任何声称「我们的 TypeScript 移植更聪明」的项目,都值得你先打个问号。

# Try the experimental release. It is a faithful port, not a fork.
npm install tsrs
npx tsrs --version

# Run it over a real project
time npx tsrs --noEmit -p tsconfig.json

二、8 倍这个数字,得看是怎么量的

官方基准的口径是清楚的:与 TypeScript 7.0 对比,相同数量的类型检查 worker。这个「相同配置」很关键,因为最近市面上出现过一批各种移植版本的性能宣传,很多是靠更大的机器规格、或者跟 TypeScript 的默认设置不一样的参数跑出来的。Cloudflare 说他们和 TypeScript 团队一起确认了对比配置一致,脚本也公开在仓库的 bench 目录里。还有两个数字更值得注意:TypeScript 7.1 beta 已经比 7.0 快最多 2.5 倍,其中一部分优化就是从 tsrs 上游回去的;而 tsrs 默认带一套随机器类型自适应的配置,会多吃内存换更快的速度。意思是:真正的性能是「你的仓库 + 你的 CI 机器 + 你的 tsconfig」三者相乘的结果,别人的图表只能当作存在可能性的证据。

# Measure both checkers on the SAME project, same worker count.
# Do not trust a vendor chart; your repo is the only benchmark that matters.

collect() {
  label=$1; shift
  /usr/bin/time -v "$@" 2>&1 >/dev/null \
    | grep -E "Elapsed \(wall|Maximum resident" \
    | sed "s/^/$label  /"
}

collect tsc  npx tsc --noEmit -p tsconfig.json
collect tsrs npx tsrs --noEmit -p tsconfig.json

# Repeat 3 times on a warm cache and take the median. A single cold run
# on a laptop will happily show you a 30% difference that is pure noise.

三、用自己的仓库量一遍

第二段脚本就干这件事,只有两条纪律:同一个项目、同样的 worker 数。跑三遍,缓存预热后取中位数。这一步看起来啰嗦,但它是唯一能避免你被单次冷启动噪声骗到的办法——笔记本上冷跑一次,能轻松给你造出 30% 的假差距。还有一件事必须做:先在仓库根目录确认 tsconfig.json 覆盖了你真正在意的代码,很多项目 typescript 检查慢不是引擎慢,而是 include 里塞了整个 node_modules 或构建产物目录。如果 tsc 慢是因为它在检查本来不该检查的文件,换引擎解决不了问题。

# Parity before speed: emit machine-readable diagnostics from both and diff them.
npx tsc  --noEmit --pretty false -p tsconfig.json > /tmp/tsc.txt  || true
npx tsrs --noEmit --pretty false -p tsconfig.json > /tmp/tsrs.txt || true

# Sort so ordering differences do not drown real differences
sort /tmp/tsc.txt  > /tmp/a.txt
sort /tmp/tsrs.txt > /tmp/b.txt
diff /tmp/a.txt /tmp/b.txt | head -40

# The number to watch is not "did it run", but
# "did it report the same errors on the same lines".

四、兼容性:先比诊断,再比速度

把 tsc 和 tsrs 都用 --pretty false 跑一遍机器可读输出,排序,再 diff。排序这一步不能省,因为错误顺序不同不代表错误内容不同,不排序的 diff 全是噪声。你要看的核心指标只有一句:同样的文件、同样的行,报的是不是同样的错。如果诊断完全一致,那才叫「只是更快」;如果多报或少报,你至少要搞清楚是哪一类规则差异,再决定能不能接受。这条路和 Cloudflare 自己维护 Vinext(让 Vinext 跟 Next.js 上游保持同步)的思路一样:和上游对齐是一个持续的过程,不是一次性的验收。

# Keep it out of your required CI job until parity holds.
# .github/workflows/typescript.yml
- name: Type check (authoritative)
  run: npx tsc --noEmit -p tsconfig.json

- name: Type check (tsrs shadow)
  continue-on-error: true
  run: npx tsrs --noEmit -p tsconfig.json

- name: Record type-check duration
  run: |
    /usr/bin/time -f "tsc_wall_seconds=%e" npx tsc --noEmit -p tsconfig.json

五、为什么选 Rust,而不是 Go

这个问题问得好:TypeScript 团队自己的 Go 移植已经让 TypeScript 快了约 10 倍、内存也省了非常多,为什么还不够?Cloudflare 的答案是 WebAssembly。Rust 编译到 Wasm 这件事有天生的结构性优势:Go 程序编到 Wasm 得把 Go 的垃圾回收器一起打进去,而 Rust 的所有权模型让 guest 和 host 之间的内存交互变得直截了当。Cloudflare Workers 早就支持 Wasm,前不久还通过 wasm-bindgen 的新 Emscripten target 支持了原生 Rust。当你的构建和部署系统在 Worker 里高频循环跑的时候,小内存占用的收益是立刻兑现的——这意味着完整、隔离的开发工具链可以跑在以前跑不了的地方,也能安全地跑在浏览器里、嵌入式设备上,甚至 IoT 里。

// The Rust + WebAssembly path is the interesting part.
// Go compiled to Wasm drags its garbage collector along; Rust does not,
// which keeps guest/host memory interaction simple.
import { createChecker } from "@tsrs/wasm";

const checker = await createChecker({
  // point at the same tsconfig your build uses
  projectPath: "/tsconfig.json",
});

const diagnostics = await checker.check();
const errors = diagnostics.filter((d) => d.category === 1);

console.log(`errors=${errors.length}`);
for (const e of errors.slice(0, 5)) {
  console.log(`${e.file}:${e.line}  TS${e.code}  ${e.message}`);
}

六、现在该怎么做

两条建议。第一,把它当影子检查跑:CI 里保留 tsc 作为权威,tsrs 用 continue-on-error,同时把类型检查耗时记成指标。等你有了几周的对照数据,再决定要不要切换。第二,关注两件正在发生的事:一是 tsrs 正在与 Theo Browne 的 tsc-rs 合并成单一包,目标是覆盖 TypeScript 全部特性,包括 LSP(Language Server Protocol)和 content mapper;合并完成前,编辑器体验不会等价,别急着把团队的 IDE 配置切过去。二是 Cloudflare 说会尝试更深的集成,比如给 Oxlint 做类型感知的 lint 规则——如果这条走通,「lint 时顺便做类型检查」可能比「单独跑一遍 tsc」更划算。顺便说一句,Cloudflare 已经是 Vite、Rolldown、Oxlint、Oxfmt 这些 JS 生态 Rust 工具的东家,平台上有超过 1000 万开发者,这条路线不是心血来潮。

基准图表与性能测量

相同 worker 数、相同配置,这样的对比才有意义

📌 常见问题 FAQ

tsrs 是 TypeScript 的一个分支(fork)吗?

不是。Cloudflare 明确表示他们对 TypeScript 做的不是 Fork,语言的设计与方向仍然由 TypeScript 团队全权决定,tsrs 的定位是严格按原始实现复刻语义的忠实移植。这条边界决定了你只应该把它当成更快的实现来评估,而不是期待它自带新特性。

8 倍到底是怎么测出来的?

官方口径是与 TypeScript 7.0 对比、类型检查 worker 数量相同。Cloudflare 表示基准配置是和 TypeScript 团队一起确认一致的,脚本公开在 oxc-project/tsrs 仓库的 bench 目录。另外 TypeScript 7.1 beta 本身已比 7.0 快最多 2.5 倍,其中部分优化来自 tsrs 上游。

现在能直接在项目里换掉 tsc 吗?

可以试,但不要立刻让它成为唯一权威。安装 tsrs 后先用 --noEmit 跑一遍,把它和 tsc 的诊断输出排序后 diff,确认报错位置与内容一致。CI 里建议 tsrs 先走 continue-on-error,跑几周对照数据再决定切换。

为什么选 Rust 而不是 Go?

核心原因是 WebAssembly。Go 编译到 Wasm 需要把垃圾回收器一起打进产物,而 Rust 的所有权模型让 WebAssembly guest 与 host 之间的内存交互更直接。这让同一套工具链既能跑在服务端沙箱里,也能跑在浏览器、嵌入式设备和 IoT 上。

编辑器里的语言服务也会跟着变快吗?

暂时不会自动获益。tsrs 正在与 Theo Browne 的 tsc-rs 合并为单一包,目标是支持 TypeScript 全部特性,包括 LSP 和 content mapper。在合并稳定之前,编辑器的类型提示体验不会与官方 tsc 等价,所以团队 IDE 配置先别急着切。