TypeScript 7 原生编译器深度实战:当 tsc 用 Go 重写,10 倍性能背后到底发生了什么?
2026 年,TypeScript 迎来自 2012 年诞生以来最大的一次架构地震——整个编译器与语言服务被用 Go 重写,代号 Project Corsa,对外版本即 TypeScript 7.0。官方演示里,对 VS Code 这种量级的巨型代码库做全量类型检查,耗时从约 77 秒掉到约 7 秒,接近 10 倍。本文不炒"性能飙升"的冷饭,而是以一个每天被
tsc拖慢十几次的一线工程师视角,把这次重写拆开揉碎:它为什么非重写不可、Go 版本到底改了哪些架构假设、你现在就能怎么用上、以及它对整个 JS 工具链意味着什么。
一、背景介绍:老 tsc 撞上的那堵墙
先说一个残酷的事实:TypeScript 的类型检查器,本质上是一个单次运行、单线程、且对内存分配极度敏感的程序。它跑在 V8 上,而 V8 是个为浏览器设计的、带分代垃圾回收(GC)的 JS 引擎。
当你的项目只有几千行、几十个文件时,这一切都不是问题。但一旦项目膨胀到几万、几十万行(想想 VS Code、想想任何一个中大型 Monorepo),三件事会同时把你按在地上摩擦:
1.1 类型检查是"伪并行"的
tsc 的类型检查遵循"先建好整棵程序符号表,再逐个文件检查"的串行模型。它不是不能并行,而是 JS 单线程 + 老架构里大量共享可变状态(shared mutable state)让它不敢并行——一旦并发改了同一份符号表,结果就不可复现了。
1.2 垃圾回收的"呼吸停顿"
类型检查过程会疯狂地创建临时对象:类型实例、约束求解的中间结果、联合类型的展开节点……在 V8 上,这意味着 GC 会周期性地"Stop-The-World"。你以为卡在类型计算,其实有一半时间 V8 在默默做垃圾回收。官方在 Corsa 的调研里就明确指出:老实现里 GC 停顿占掉了可观的比例。
1.3 结构性类型系统的 O(n²) 陷阱
TS 是结构化类型系统(structural typing):判断 A 能否赋值给 B,要递归比较它们的形状。当你写出深度嵌套的条件类型 + 超大联合类型时,检查器的复杂度会飙到平方级甚至指数级。配合单线程 + GC,结果就是"改一行类型,等半分钟"。
// 一个会让老 tsc 明显变慢的典型"类型体操"场景
type DeepGet<T, P extends string> =
P extends `${infer K}.${infer Rest}`
? K extends keyof T
? DeepGet<T[K], Rest>
: never
: P extends keyof T
? T[P]
: never;
// 当 T 是一个有几百个字段的巨型接口、P 是长路径字符串时,
// 条件类型的递归展开会在老实现里产生大量临时类型对象,
// 单线程 + GC 让它慢得肉眼可感。
核心矛盾:问题的根源不是"TS 语言设计错了",而是"承载它的运行时(V8 + 单线程 JS)撑不住现代代码库的规模"。重写编译器,而不是重写语言,才是正解。
二、核心概念:Project Corsa 到底重写了什么
Project Corsa 不是给 TS 加一堆新语法糖,它是把现有 TypeScript 编译器的实现,从 TypeScript/JavaScript 平移到 Go,目标是:
在行为(输出类型错误、产出
.js)与现有tsc完全等价的前提下,把吞吐量和编辑器响应速度提升一个数量级。
这里有几个关键认知,很多人会搞混:
2.1 它重写的是"实现",不是"语言"
你写的 .ts 文件、你的 tsconfig.json、你的类型体操——全部不变。Corsa 保证的是"同一个输入,产出同样的类型检查结果"。它是一条平行赛道,先做到 100% 行为对齐,再择机切换为默认。
2.2 为什么是 Go,不是 Rust,也不是 C++
社区最爱吵这个。Anders Hejlsberg 团队的公开表述是:选 Go 主要是工程现实——TS 团队本身就写 Go,Go 的语法心智模型与 TS 编译器原有的数据结构高度吻合,迁移成本最低;同时 Go 提供了真正的原生多线程(goroutine)和可控得多的内存/GC 模型,这正是解决"并行检查"和"GC 停顿"两大致命伤的钥匙。
一句话:Rust 性能上限更高,但 Go 的"够快 + 团队上手快 + 并发模型直白"在这个特定项目里是更优解。
2.3 两个名字:tsc 与 tsgo
- 老实现:
tsc(TypeScript 编译器,跑在 Node/V8 上) - 新实现:
tsgo(TypeScript compiler in Go),一个原生二进制
发布流程里你会看到 tsgo 这个命令。它和 tsc 接受几乎一样的参数,只是底层是 Go 编译出的机器码。
三、架构分析:10 倍性能到底从哪来
性能不是魔法,是四个具体的架构决策叠加出来的。我们逐个拆。
3.1 真正的并行类型检查
这是最大的杀手锏。Go 版本把"程序符号表"与"单文件检查"解耦,让多个文件可以同时被检查。示意模型(注意:以下为说明并发思路的伪代码,非真实源码):
// 示意:tsgo 如何把文件检查并发化(概念模型,非源码)
func (p *Program) CheckFiles(files []SourceFile) []Diagnostic {
results := make(chan fileResult, len(files))
var wg sync.WaitGroup
for _, f := range files {
wg.Add(1)
go func(f SourceFile) {
defer wg.Done()
// 每个文件独立检查,读取共享但不可变的符号表
diags := checkFile(p.readOnlySymbolTable, f)
results <- fileResult{file: f, diags: diags}
}(f)
}
go func() { wg.Wait(); close(results) }()
var all []Diagnostic
for r := range results {
all = append(all, r.diags...)
}
return all
}
关键在 readOnlySymbolTable(只读符号表):因为检查阶段不再修改全局状态,多个 goroutine 才能安全地并发读。这正是老 JS 实现因为大量可变共享状态而不敢做的点。
3.2 告别 V8 的 GC 呼吸停顿
Go 的 GC 虽然也是并发标记-清扫,但 Go 运行时对短生命周期对象的分配远快于 V8,且停顿通常控制在亚毫秒级。类型检查产生的海量临时类型对象,在 Go 上"生灭"的代价远低于在 V8 上。这省掉的就是老 tsc 里那块看不见的 GC 时间。
3.3 数据局部性与值语义
TS 编译器的核心数据结构(AST 节点、类型对象)在 Go 里可以用值语义 + 紧凑结构体表达,内存布局连续、缓存命中率高。而 JS 里一切皆对象、皆指针、皆走 V8 的隐藏类(hidden class)查找——同样的逻辑,Go 版的 CPU 缓存友好度天然高一截。
3.4 启动零解释开销
tsc 每次运行,V8 都要先把编译器本身的 JS 代码解析 + 编译 + 优化一遍(JIT warm-up)。tsgo 是编译好的原生二进制,启动即干活。对一个"保存文件 → 立即类型检查"的编辑器场景,这部分冷启动差的累积效应非常可观。
把这四点叠起来:并行(≈核心数倍加速)× 无 GC 停顿 × 缓存友好 × 零冷启动 ≈ 官方宣称的 10 倍。注意它不是"每个项目都正好 10 倍"——小项目提升有限,项目越大、类型越重,倍数越夸张。
四、代码实战:现在就把 10 倍性能装进你的工作流
好消息:你今天就能用上预览版,不需要等 7.0 正式 GA。
4.1 安装原生预览
官方提供了一个预览包,让你用熟悉的方式调用 tsgo:
# 不污染全局,用 npx 直接跑原生预览版编译器
npx @typescript/native-preview tsc --version
# 对你的项目做全量类型检查(参数与 tsc 几乎一致)
npx @typescript/native-preview tsc -p tsconfig.json --noEmit
提示:具体包名与命令请以 TypeScript 官方博客 /
typescript-go仓库为准,预览期命令可能微调。本文给出的是稳定可用的调用形态。
4.2 跑一个真实对比基准
别信宣传数字,自己量。用 hyperfine 在同一台机器上对比老 tsc 与 tsgo:
# 安装基准测试工具
npm i -D hyperfine
# 对同一份 tsconfig 做冷暖对比
hyperfine --warmup 3 --runs 5 \
'npx tsc -p tsconfig.json --noEmit' \
'npx @typescript/native-preview tsc -p tsconfig.json --noEmit'
典型输出会是这样(示意,数字因项目而异):
Benchmark 1: npx tsc -p tsconfig.json --noEmit
Time (mean ± σ): 41.2 s ± 1.1 s
Range (min … max): 40.0 s … 43.0 s
Benchmark 2: npx @typescript/native-preview tsc -p tsconfig.json --noEmit
Time (mean ± σ): 4.8 s ± 0.3 s
Range (min … max): 4.5 s … 5.3 s
Summary
'tsgo' ran 8.58 ± 0.45 times faster than 'tsc'
当你看到自己项目从 40 秒掉到 5 秒,那种"终于不用盯着转圈圈发呆"的爽感,比任何文章都有说服力。
4.3 在 CI 里先吃螃蟹
最稳妥的落地姿势:CI 的 type-check 阶段先用预览版。它对产出零影响(行为对齐),却能把流水线的类型检查时间砍掉一大截。
# .github/workflows/ci.yml 片段
- name: Type check (native preview)
run: npx @typescript/native-preview tsc -p tsconfig.json --noEmit
配合 package.json 脚本:
{
"scripts": {
"typecheck": "tsc -p tsconfig.json --noEmit",
"typecheck:go": "npx @typescript/native-preview tsc -p tsconfig.json --noEmit"
}
}
4.4 让编辑器也快起来(原理)
VS Code 的类型检查与自动补全,依赖 TS 的语言服务(Language Service)。Corsa 同样重写了语言服务(用 Go)。在预览期,你可以把 VS Code 指向预览版的语言服务,体验"保存即反馈、不再转圈"。
// .vscode/settings.json
{
// 指向本地的原生预览版 tsserver(路径以官方文档为准)
"typescript.tsdk": "node_modules/@typescript/native-preview/lib"
}
这一步是预览特性,生产项目请评估稳定性后再全员推广。但对个人开发机,它带来的"丝滑感"值得一试。
五、性能优化:把 10 倍榨到 15 倍,以及别踩的坑
光换编译器不够,配合几个工程习惯,收益还能再放大。
5.1 永远开 --incremental
原生版本对增量检查(.tsbuildinfo)的支持更彻底。第二次检查只跑变更波及的部分:
npx @typescript/native-preview tsc -p tsconfig.json --noEmit --incremental
配合 tsconfig.json:
{
"compilerOptions": {
"incremental": true,
"composite": true, // Monorepo 引用项目必备
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo"
}
}
5.2 Monorepo 用 Project References 切分
tsgo 的并行能力在"多项目"结构下发挥最充分。把巨型项目拆成若干 composite 子项目,Go 版可以跨项目更智能地调度:
// 根 tsconfig.json
{
"files": [],
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/ui" },
{ "path": "./packages/app" }
]
}
# 并行构建所有引用项目
npx @typescript/native-preview tsc -b
5.3 用基准驱动,别靠感觉
把 4.2 的 hyperfine 命令固化进一个 scripts/bench-ts.sh,每次升级编译器或改 tsconfig 都跑一遍。性能是否回退,数据说话:
#!/usr/bin/env bash
# scripts/bench-ts.sh
set -euo pipefail
hyperfine --warmup 2 --runs 3 \
'npx tsc -p tsconfig.json --noEmit' \
'npx @typescript/native-preview tsc -p tsconfig.json --noEmit'
5.4 务必注意的三个坑
- 行为对齐是目标,不是已完成态。预览期个别边界场景(极复杂的类型推导、某些
lib.d.ts交互)可能与老tsc有细微差异。生产 CI 同时保留老tsc跑一遍做回归,是稳妥做法。 - 不是所有慢都是编译器慢。如果你的瓶颈是
tsc之前的打包/转译(如 webpack 冷启),换tsgo帮不了你——那是打包器的事(见下一节对工具链的延伸)。 - 小项目收益有限。几百个文件以内的项目,10 倍听起来香,实际可能只是从 800ms 变 120ms,体感提升远不如巨型项目明显。
六、总结展望:不止一个编译器,而是一整条工具链的"原生化"
把视野拉高一层,TypeScript 7 不是一个孤立事件。它和这几年 JS 工具链的集体转向,是同一个故事的不同章节:
- esbuild(Go)把打包/转译拉进亚秒时代;
- swc(Rust)让 Next.js 的转译快到飞起;
- oxc / rolldown(Rust)正在重写整个 JS 工具链的底层;
- 现在,TypeScript 自己(Go)也下了场。
趋势非常清晰:语言 / 运行时是 JS,但承载它的开发者工具,正在全面迁向 Go / Rust 这类能编译成原生机器码、带真并行的系统级语言。原因只有一个——当代码库规模突破某个临界点,V8 单线程 + GC 的物理上限,挡住了工程师的生产力。
对一线开发者,我的判断是:
- 短期(2026):把
tsgo当 CI 加速器用,本地编辑器可尝鲜,生产代码零改动。这是风险最低、收益最高的切入点。 - 中期:随着行为对齐成熟,包管理器/脚手架会默认把类型检查指向原生实现,你甚至感知不到切换。
- 长期:类型检查从"喝杯咖啡的等待"变成"保存即完成",编辑器里的类型反馈会像本地补全一样即时。TS 团队自己也放话,目标是把大型项目的全量检查压进亚秒级。
最后说句大实话:作为一个被 tsc 拖慢过无数次的人,我不在乎它是用 Go 还是 Rust 写的,我在乎的是——当我按下保存,红色波浪线能不能在 100 毫秒内出现。Project Corsa 给出的答案,正在从"梦想"变成"可测量的基准数字"。这,才是这次重写真正值得兴奋的地方。
参考资料与延伸:TypeScript 官方博客 Project Corsa 公告、TypeScript Go 移植仓库(typescript-go)、VS Code 团队关于原生语言服务的说明。文中性能数字为官方演示与社区基准的典型区间,具体数值以你自己的 hyperfine 实测为准。