MLog
返回文章列表
前端#TypeScript#Go#编译器#性能优化

TypeScript 7.0 编译器 Go 重写:一场持续了十年的工程债终于还清

发布于: 2026年7月15日阅读时长: 12 min

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。

这个决定透露了几件事:

  1. 他们不想引入 C++ 工具链依赖——TypeScript 是一个 npm 包,需要能轻松编译到所有平台。Go 的交叉编译在这里是天然的解决方案。
  2. 他们认可 Parcel watcher 的设计——这个组件在 VS Code 中已经用了很多年,经过了大规模验证。
  3. 移植完成后,他们又把它进一步「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 中的弃用配置变成了硬错误。你应该:

  1. 先升级到 6.0,处理所有 deprecation 警告
  2. 确认 rootDirtypesmoduleResolution 等关键配置正确
  3. 在 CI 跑一次全量类型检查
  4. 然后安装 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 作斗争。

这不是一个性能版本。这是一个为下一个十年铺路的版本。