编程 TypeScript 7 原生编译器深度实战:当 tsc 用 Go 重写,10 倍性能背后到底发生了什么?

2026-07-10 12:43:07 +0800 CST views 11

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 两个名字:tsctsgo

  • 老实现: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 在同一台机器上对比老 tsctsgo

# 安装基准测试工具
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 务必注意的三个坑

  1. 行为对齐是目标,不是已完成态。预览期个别边界场景(极复杂的类型推导、某些 lib.d.ts 交互)可能与老 tsc 有细微差异。生产 CI 同时保留老 tsc 跑一遍做回归,是稳妥做法。
  2. 不是所有慢都是编译器慢。如果你的瓶颈是 tsc 之前的打包/转译(如 webpack 冷启),换 tsgo 帮不了你——那是打包器的事(见下一节对工具链的延伸)。
  3. 小项目收益有限。几百个文件以内的项目,10 倍听起来香,实际可能只是从 800ms 变 120ms,体感提升远不如巨型项目明显。

六、总结展望:不止一个编译器,而是一整条工具链的"原生化"

把视野拉高一层,TypeScript 7 不是一个孤立事件。它和这几年 JS 工具链的集体转向,是同一个故事的不同章节:

  • esbuild(Go)把打包/转译拉进亚秒时代;
  • swc(Rust)让 Next.js 的转译快到飞起;
  • oxc / rolldown(Rust)正在重写整个 JS 工具链的底层;
  • 现在,TypeScript 自己(Go)也下了场。

趋势非常清晰:语言 / 运行时是 JS,但承载它的开发者工具,正在全面迁向 Go / Rust 这类能编译成原生机器码、带真并行的系统级语言。原因只有一个——当代码库规模突破某个临界点,V8 单线程 + GC 的物理上限,挡住了工程师的生产力。

对一线开发者,我的判断是:

  1. 短期(2026):把 tsgo 当 CI 加速器用,本地编辑器可尝鲜,生产代码零改动。这是风险最低、收益最高的切入点。
  2. 中期:随着行为对齐成熟,包管理器/脚手架会默认把类型检查指向原生实现,你甚至感知不到切换。
  3. 长期:类型检查从"喝杯咖啡的等待"变成"保存即完成",编辑器里的类型反馈会像本地补全一样即时。TS 团队自己也放话,目标是把大型项目的全量检查压进亚秒级

最后说句大实话:作为一个被 tsc 拖慢过无数次的人,我不在乎它是用 Go 还是 Rust 写的,我在乎的是——当我按下保存,红色波浪线能不能在 100 毫秒内出现。Project Corsa 给出的答案,正在从"梦想"变成"可测量的基准数字"。这,才是这次重写真正值得兴奋的地方。


参考资料与延伸:TypeScript 官方博客 Project Corsa 公告、TypeScript Go 移植仓库(typescript-go)、VS Code 团队关于原生语言服务的说明。文中性能数字为官方演示与社区基准的典型区间,具体数值以你自己的 hyperfine 实测为准。

推荐文章

10个极其有用的前端库
2024-11-19 09:41:20 +0800 CST
Vue3中的事件处理方式有何变化?
2024-11-17 17:10:29 +0800 CST
OpenCV 检测与跟踪移动物体
2024-11-18 15:27:01 +0800 CST
网站日志分析脚本
2024-11-19 03:48:35 +0800 CST
JavaScript设计模式:桥接模式
2024-11-18 19:03:40 +0800 CST
程序员茄子在线接单