Skip to content

TypeScript 编译器的 Go 语言移植详解

背景介绍

目前没有任何工具能够完全替代 tsc 提供的全面类型检测能力

微软今日发布公告:

[...] 我们已经开始对 TypeScript 编译器和工具进行原生移植。这一原生实现将大幅改善编辑器启动时间,将大多数构建时间减少约 10 倍,并显著降低内存使用量。

本文将深入探讨这一重大消息背后的技术细节。

代码库:JavaScript 与原生实现对比

为避免混淆,我将使用以下术语:

  • JavaScript 代码库:当前的 TypeScript 代码库 — 实际上是用 TypeScript 编写的。
  • Native 代码库:新代码库。我使用"原生"这一术语是因为 TypeScript 团队也是这样称呼的 - 实际上是用 Go 编写的。

为何这是重大突破

类型检查是外部工具无法完成的唯一任务:

  • 生成 .js 文件已经变得更快 — 得益于 原生工具类型剥离
  • 生成 .d.ts 文件已经变得更快 — 得益于 原生工具隔离声明

因此,类型检查同样变得更快是一个重大进展。

项目时间线

当前 TypeScript 版本为 5.8。

TypeScript 6 (JavaScript): JavaScript 代码库将继续用于 6.x 系列,6.0 版本将引入一些突破性变更以便与原生代码库保持一致。

  • TypeScript 原始代号:Strada

TypeScript 7 (原生): 一旦原生代码库达到与 JavaScript 代码库的足够对等性,它将作为 TypeScript 7.0 发布。

  • 原生代码库代号:Corsa

两个代码库将长期共存。

需要迁移的内容

了解 TypeScript 生态系统中哪些部分需要迁移是很有帮助的:

  • 命令行 TypeScript 编译器
  • TypeScript 语言服务器(帮助编辑器支持 TypeScript
    • JavaScript 代码库早于广泛使用的语言服务器协议,因此不使用该协议。新的语言服务器将使用该协议,这应该使编辑器更容易支持 TypeScript
  • 使用 TypeScript 代码库的工具
    • Go 代码库的交互需要全新的方法:
      • 内部组件暴露将减少
      • 交互现在跨进程进行

原生版本何时可供公众使用

当前状态:tsc 可用。仍缺少:

  • JSX
  • 通过 JSDoc 的类型
  • 构建模式(项目引用)

2025 年中:带有 JSXJSDoctsc(不含构建模式)2025 年底:完整 tsc 和语言服务器

来源:"A 10x Faster TypeScript"

项目何时开始

由 Anders Hejlsberg 开发的第一个原型:

  • 始于 2024 年 8 月
  • 扫描器和解析器花了 1-1.5 个月编写
  • 初始方法:手动编写代码
  • 后来:自动将 TypeScript 代码转换为 Go 代码的工具
  • 移植代码效果良好(需要一些人工干预)
  • 移植数据结构只能由人工完成 — 因为 JavaScript 的对象(具有灵活类型)和 Go 的结构体(具有高度可配置的数据布局)非常不同。此外,它们现在必须在并发环境中工作 — 例如:JavaScript 代码库通过在创建类型时给每个类型一个序列号来排序类型。这种方法在 Go 代码库中不适用,因为由于多线程的参与,类型创建的顺序不再是确定性的。

为何选择 Go 而非其他编程语言

TypeScript 团队希望(主要)移植 JavaScript 代码库,而不是用不同的语言重写 — 原因有两个:

  1. 新代码库必须(主要)是旧代码库的即插即用替代品。而这通过重写很难实现。
  2. 重写耗时更长。

如果我们看一下用于新代码库的编程语言的要求,其中一些源于移植的决定:

  • 支持循环数据结构 — TypeScript 代码库大量使用。
  • 垃圾回收。代码库假定有此功能。
  • JavaScript 代码库的风格比面向对象编程更偏向函数式,不经常使用类。这种风格类似于 Go 的编写方式。

其余要求由性能和易用性(开发者体验)驱动:

  • 在所有主要平台上有良好的原生代码支持。
  • 语言应简单易学。
  • 语言应有良好的工具支持。
  • 对内存中数据结构布局的控制。使用 Go,你可以使用结构体并创建一个结构体数组,只需一次分配(相比 JavaScript 中的多次分配)。
  • 对共享内存并发的良好支持 — 这是使代码更快的重要元素(下面会详细介绍)。

为何不选择 C#

当被问到"为何不选择 C#"时,Anders Hejlsberg 提到以下几点:

  • GoC# 更低级。
  • Go 对生成原生代码有更好的支持(包括指定数据结构的布局)。
  • Go 更适合 JavaScript 代码库中使用的非面向对象编程风格。

10 倍性能提升从何而来

  • 一半的速度提升来自共享内存并发和使用多核。
  • 另一半来自原生代码:JavaScript 必须实时编译;它必须为其对象提供大量灵活性;它不能内联对象(每个数组元素一次分配 vs. 整个数组一次分配);等等。

JavaScript 确实通过 Web Workers 有并发能力,但内存共享非常有限(参见 SharedArrayBuffer)。

TypeScript 编译有以下阶段:

  1. 解析:生成抽象语法树 (AST)
  2. 绑定:为声明创建"符号表",设置控制流图等
  3. 类型检查
  4. 发射:代码生成

在原生代码库中,解析和绑定可以独立完成(不需要内存共享)。然后,数据结构是不可变的,可以在线程之间轻松共享。

解析、绑定和发射的速度与使用的核心数量成线性关系。它们共同占总编译时间的约三分之一。

类型检查占剩余的三分之二,不太容易并行化。因此,使用以下技巧:

  • 类型检查适用于单个文件 — 它根据需要延迟加载更多信息。
  • 技术:运行多个类型检查器,给每个检查器分配部分文件。
  • 线程安全需求不高:检查器只共享不可变的抽象语法树。有些工作是重复的,但不多,因为大部分类型信息是本地的。
  • 另一方面,线程不能完全独立操作 — 例如,错误消息不应重复且应以确定的顺序显示。
  • 使用 4 个检查器(当前硬编码数量),由于工作重复,内存使用增加 20%,但检查速度提高 2-3 倍。
  • 注意,这 20% 是相对于单检查器 Go 的 — 而单检查器 Go 使用的内存只有 JavaScript 代码库的一半。
  • 在测试中,使用 8 个检查器仅带来额外 20% 的速度提升(来源)。

原生代码库是否可运行于 WebAssembly

支持 WebAssembly 很重要,因为它能够实现在线 TypeScript 游乐场等用例。实际上已经支持了。

Kevin Deng 编写了一个"TypeScript Go Playground",使用编译到 WebAssembly 的新代码库。

改进 GoWebAssembly 输出大小和性能的工作正在进行中 — 即:它们可以而且将会变得更好。GitHub 上相关讨论:"Go Wasm performance"

结论:一项令人印象深刻的成就

我对 TypeScript 团队能够如此快速地将 JavaScript 代码库移植到 Go 印象深刻。在一个播客中(见下面的"信息来源"),Hejlsberg 说"我在8月开始原型设计"。我没想到他指的是 2024 年,而是 2023 年甚至更早。

这种速度证明了团队方法的巧妙:如果他们重写 JavaScript 代码库而不是移植它,可能需要数年时间,并导致代码库之间的许多不一致性。

Anders Hejlsberg 表达的一个有趣担忧是,随着类型检查变得更快,人们可能会停止尝试编写可以快速计算的类型 — 例如,使用模板字面量类型很容易创建需要大量计算能力的类型。

也许我们最终会获得分析类型性能的工具。类型级调试也似乎很有用。

信息来源

  • 视频 "Syntax podcast: Typescript Just Got 10× Faster",由 Wes Bos 和 Scott Tolinski 主持,与 Anders Hejlsberg 和 Daniel Rosenwasser 对话
  • 视频 "TypeScript is being ported to Go | interview with Anders Hejlsberg",由 Dimitri Mitropoulos 为 Michigan TypeScript 制作
  • 博客文章 "A 10x Faster TypeScript",作者 Anders Hejlsberg
  • 视频 "Anders Hejlsberg on TypeScript's Go Port",由 Matt Pocock 主持
  • GitHub 讨论: "Why Go?"

Contributors

Changelog

Discuss

Released under the CC BY-SA 4.0 License. (2619af4)