TypeScript 7.0 编译器 Go 重写:一场持续了十年的工程债终于还清
TypeScript 7.0 RC 发布,编译器从 TypeScript 移植到 Go,大型项目构建提速 8-12 倍。本文从工程决策角度,聊聊这次重写背后的技术选择、性能来源和生态影响。
上周 TypeScript 7.0 RC 发布了。新闻标题都在说「10 倍提速」,但这其实不是最值得聊的部分。
我最近把一个中等规模的 monorepo(大概 2000 多个 .ts 文件)从 6.0 升到了 7.0 RC。tsc --noEmit 从 47 秒降到了 5 秒出头。说实话,我一开始以为 benchmark 有水分,结果跑完第一遍就信了。
但真正让我感兴趣的,不是这个数字本身,而是 TypeScript 团队做的一系列工程决策——为什么选 Go 而不是 Rust?怎么解决并行类型检查的正确性问题?文件监听为什么需要重写?这些选择背后的逻辑,比 10x 这个数字更有价值。
不是「新版本」,是「新材料」
先搞清楚一个事实:TypeScript 7.0 并没有引入什么新的类型系统特性。它的类型检查逻辑和 6.0 在结构上完全一致。用 TypeScript PM Daniel Rosenwasser 的话说,这是一次「方法式移植」,不是重写。
什么意思?就是把原来用 TypeScript 写的编译器逻辑,一行一行地用 Go 重新实现了一遍——保持相同的架构、相同的类型推导路径、相同的错误信息格式,只是运行环境从 V8 换成了 Go 的原生二进制。
这跟 esbuild 的思路完全不一样。esbuild 是 Evan Wallace 用 Go 从零写了一个 bundler,自己设计数据结构和算法。TypeScript 7.0 的做法更像是在做一次大规模的 transcompilation——目标不是重新发明类型检查,而是让现有的类型检查跑得更快。
这个区别很重要,因为它解释了为什么 TypeScript 团队能在相对短的时间内完成迁移(从 2025 年底宣布到 2026 年 6 月 RC),也解释了为什么新编译器几乎没有引入行为变化——因为它本质上就是同一个编译器,只是换了发动机。
性能到底从哪来?
很多人看到「10x」就觉得是 Go 比 JavaScript 快 10 倍,这其实是对性能来源的严重误解。V8 的 JIT 编译已经能把 JavaScript 跑得很快了,单线程的纯计算任务,Go 不会比 V8 快一个数量级。
真正的收益来自三点:
第一,原生并行。 TypeScript 的构建管道有三个阶段:解析(parsing)、类型检查(type-checking)、代码生成(emit)。在 JavaScript 版本中,这三个阶段跑在同一个主线程上。而在 Go 版本中,解析和 emit 在不同文件之间几乎完全独立,可以无摩擦地并行化。
真正棘手的是类型检查。一个文件的类型信息依赖于它所导入的其他文件,你不能简单地把文件随机分配给不同线程。TypeScript 7.0 的解法很聪明:创建固定数量的 worker(默认 4 个),每个 worker 有自己完整的「世界观」——它们可能会重复做一些公共工作(比如检查全局类型声明),但因为输入相同、划分策略一致,结果一定是确定的。
这不是理论上最优的并行化方案(有重复计算),但它在工程上是正确的——不需要引入复杂的依赖图调度,不需要处理分布式一致性,直接就能工作。
第二,没有 GC 停顿。 JavaScript 的 GC 在大内存场景下是个真实问题。TypeScript 编译器处理大型代码库时,峰值内存经常以 GB 计,GC 停顿会显著影响端到端延迟。Go 的并发 GC 虽然也不是零开销,但在这种场景下明显更可控。
第三,启动时间。 tsc 作为一个 CLI 工具,每次调用都需要 Node.js 加载整个运行时——解析 tsconfig、初始化模块系统、JIT 预热。而 Go 编译出的原生二进制,启动几乎是瞬间的。对于 CI 环境中频繁调用的 tsc --noEmit,这个差距会被放大很多倍。
实际数据(来自官方公告):
| 项目 | TypeScript 6 | TypeScript 7 | 加速 |
|---|---|---|---|
| VS Code | 125.7s | 10.6s | 11.9x |
| Sentry | 139.8s | 15.7s | 8.9x |
| Bluesky | 24.3s | 2.8s | 8.7x |
| Playwright | 12.8s | 1.47s | 8.7x |
| tldraw | 11.2s | 1.46s | 7.7x |
VS Code 的编辑体验变化尤其直观:打开一个带错误的文件,到出现首个诊断信息的时间从 17.5 秒降到了 1.3 秒。这不是「快了」,这是从「可以忍受」变成了「即时反馈」。
一个被低估的故事:Parcel watcher 的跨语言移植
tsc --watch 的文件监听在旧版本中一直是个性能瓶颈。Node.js 的 fs.watch 在不同操作系统上行为不一致,在大项目的 node_modules 目录下开销巨大。
TypeScript 团队需要一个高效、跨平台的文件系统监听方案。他们的选择很有意思:把 Parcel 的 watcher(一个成熟的 C++ 实现)移植到 Go。
这个决定透露了几件事:
- 他们不想引入 C++ 工具链依赖——TypeScript 是一个 npm 包,需要能轻松编译到所有平台。Go 的交叉编译在这里是天然的解决方案。
- 他们认可 Parcel watcher 的设计——这个组件在 VS Code 中已经用了很多年,经过了大规模验证。
- 移植完成后,他们又把它进一步「Go 化」——从直译逐步重构为更符合 Go 习惯的实现。这是工程上的务实主义:先用能跑起来的,再优化成更好看的。
Parcel 的作者 Devon Govett 被专门致谢。这种 C++ → Go → TypeScript 的反馈循环,在开源生态中并不常见,但很值得尊重。
为什么是 Go 而不是 Rust?
这是 TypeScript 团队公布 Go 移植计划以来被问得最多的问题。JavaScript 生态里,esbuild 用 Go,swc 用 Rust,Rome/Biome 用 Rust,Oxc 用 Rust,Rolldown 用 Rust。看起来 Rust 才是「正确」答案。
我的理解是:TypeScript 编译器不是 bundler,也不是 parser 库。它是一个类型系统——需要处理极其复杂的控制流分析、泛型推断、条件类型分发。这种逻辑的特点是高度分支化、状态密集、难以用 Rust 的 ownership 模型优雅表达。
Rust 的 borrow checker 会让这种「从 JavaScript 直译类型检查逻辑」的过程变得极其痛苦。你需要重新设计数据结构和所有权模型,这本质上就不是移植了,而是重写。而 TypeScript 团队明确表示,他们不想重写——他们要的是「相同语义、更快执行」。
Go 的 goroutine + channel + 共享内存并发模型,加上简单的语法规避了 ownership 问题,使得直译成为可能。这是一个务实的工程选择:用 Rust 可能得到更极致的性能,但需要 2-3 倍的工作量,而且有引入行为差异的风险。
更大的图景:JS 基础设施正在离开 JavaScript
TypeScript 7.0 不是孤例。看看过去几年发生了什么:
| 项目 | 原始语言 | 目标语言 | 状态 |
|---|---|---|---|
| esbuild | — | Go | 生产使用 |
| swc | — | Rust | 生产使用 |
| Rome → Biome | TypeScript | Rust | 生产使用 |
| Oxc | — | Rust | 生产使用 |
| Rspack | — | Rust | 生产使用 |
| Rolldown | — | Rust | 开发中 |
| Parcel 2 | JavaScript | Rust | 已发布 |
| TypeScript (tsc) | TypeScript | Go | 7.0 RC |
这个趋势不是巧合。构建工具、编译器、linter 有一个共同特征:它们是「CPU 密集型 + 高度可并行 + 可缓存」的工作负载,恰好是 JavaScript 运行时最不擅长的那一类。JavaScript 在设计上就不是为这种场景优化的——它擅长的是 I/O 密集的异步任务(这是 Node.js 的立身之本),而不是把 8 个核心同时跑满。
换句话说,不是 JavaScript 不好,而是用螺丝刀钉钉子确实不太对劲。
迁移:先到 6.0,再到 7.0
如果你还在用 TypeScript 5.x,不要直接跳到 7.0。TypeScript 7.0 把 6.0 中的弃用配置变成了硬错误。你应该:
- 先升级到 6.0,处理所有 deprecation 警告
- 确认
rootDir、types、moduleResolution等关键配置正确 - 在 CI 跑一次全量类型检查
- 然后安装 7.0 RC:
npm install -D typescript@rc
如果你的项目依赖 typescript-eslint 或其他编译器 API 使用者,注意 7.0 不提供稳定的程序化 API——这要等到 7.1。过渡期可以用 @typescript/typescript6 兼容包让工具链继续工作:
{
"devDependencies": {
"typescript": "npm:@typescript/typescript6@^6.0.2",
"@typescript/native": "npm:typescript@^7.0.2"
}
}
这不是长期方案,只是给生态工具争取适配时间的过渡策略。
结语
TypeScript 7.0 最让我兴奋的,不是现在能看到的那 10 倍性能提升,而是这层 Go 基础给未来释放的工程自由度。
过去十年,TypeScript 团队每加一个新特性,都要小心翼翼地评估「这会让 tsc 变慢多少」。现在这个约束被解除了。后续版本可以更激进地改进类型推断、增加更智能的错误诊断、探索更大规模的增量构建——而这些都不再需要跟 V8 的 GC 作斗争。
这不是一个性能版本。这是一个为下一个十年铺路的版本。