TypeScript-Go 编译器,有人用 C# 重写吗?性能怎么样?

Anders 当年为什么不选 C#

2025 年初,微软宣布把 TypeScript 编译器从 TypeScript 自己移植成原生代码,语言选的是 Go。

这个消息在 .NET 圈子里多少有点微妙。Anders Hejlsberg 是 C# 的首席设计师,也是 TypeScript 之父,结果亲儿子要换新家,选的却不是另一个亲儿子。官方 Why Go? 的讨论帖下面,C# 相关的问题被问了一遍又一遍。

后来 Anders 亲自下场,回了一段很长的话。核心是这几句:

In a green field, this would have been a totally different conversation. But this was not a green field - it's a port of an existing codebase with 100 man-years of investment.
如果是从零开始,那会是完全不同的讨论。但这不是从零开始------这是对一个已经投入了 100 人年的代码库做移植。

他还提到,团队评估过很多语言、做过好几个原型,其中就有 C#。最后选 Go,是因为 TypeScript 编译器的代码里全是函数和数据结构,几乎没有类,而地道的 Go 代码看起来跟它几乎一模一样,移植可以做到一一对应,甚至能大量自动化。

换句话说,这次是移植(port) ,不是重写(rewrite)。只有逐行移植,才能保住现有编译器的语义、算法和每一个报错行为,而全世界的项目都依赖这些行为。

于是,一支世界顶级的编译器团队,花了一年多时间,把这份 100 人年的家底一行一行地搬进了 Go。

这个决定我是服气的。只是心里总有个念头:如果当年选的是 C#,现在会怎样?

以前这个问题没法回答,因为没人会再花一年多时间去验证一个假设。但 2026 年,情况变了。

42 万美元的 Rust

先说一个不是 C# 的故事。

YouTube 网红 Theo(就是 T3 Stack 那位)做了 ts-rust:用大模型把 TypeScript 的 Go 版编译器整个移植成 Rust。README 第一句就很直接:

It cost over 420,000 in tokens to do it, but you could probably have done it for \~20k.

他先用 OpenAI 的模型跑了几个月,烧掉了 40 多万美元的 API 等价额度,写了 130 多万行 Rust,兼容性一直卡在 84% 左右。后来换成 Claude Opus 5.5 从头开始,10 小时出了能用的 v0,两周、约 2.4 万美元把它做完了。

看到这个数字,我的第一反应是:

他还专门拍了一期视频,标题就叫"You can rewrite typescript in rust for $420k":

视频里讲了一个很有意思的细节(14:00 前后):Opus 并没有"用 Rust 重新设计",而是把 Go 版一行一行、忠实地搬了过去,连 Go 的标准库和 goroutine 都在 Rust 里重新实现了一遍。这正是微软团队当年从 JS 到 Go 的做法------先逐行移植,再慢慢打磨。

所以第一版 ts-rust 其实比 Go 版慢两到三倍。Theo 随后给 AI 定了一个目标:"每一项都至少快两倍",然后让它自己跑了两周。

结果是:ts-rust 不光跑通了 18 万多个移植过来的 Go 测试,速度还反超了官方 Go 版。按 Theo 的说法,这 2.4 万美元的 API 等价开销,实际走的是 Claude 订阅,折算下来也就 500 美元上下。

这件事说明了三点:

  1. 用 AI 把 TypeScript 编译器整个搬到另一门语言,已经不是天方夜谭。
  2. 方法比钱重要。同样的目标,走错路烧 40 万,走对路两万多。
  3. 顺序很重要:先逐行移植、把行为对齐,再谈优化。第一版比 Go 慢两三倍,一点都不丢人。

还有一个细节:Theo 在视频开头就说了,整个项目他一行代码都没看过。

回头再看 Anders 的理由

用今天的眼光再读一遍 Anders 那段话,会发现它一半被 AI 改写了,一半反而更成立了。

被改写的那一半:选 Go,是因为原代码和地道的 Go 长得一样,人工移植最省事。这个理由的前提是"移植主要靠人"。可 Go 和 Rust 差得够远了------一个有 GC,一个没有;一个随手开 goroutine,一个要跟借用检查器较劲------AI 照样逐行搬了过去,而作者一行没看。下面会看到,搬到 C# 的成本更是低到一两百美元。"语言长得像不像",已经不再是决定性的因素。

更成立的那一半:所有这些 AI 移植,起点都是 Go 版,而不是原来的 JS 版。Theo 自己也说,让 AI 直接从 JS 移植到 Rust 是做不成的,从 Go 出发却相对轻松。而"一行不看"之所以敢这么干,靠的是测试:ts-rust 跑通了 18 万多个移植过来的 Go 测试。那 100 人年并没有消失,它变成了 Go 版的代码和测试,成了今天所有 AI 移植的"标准答案"。

所以,Anders 当年的选择没错,而且正是那次人工逐行移植,给后来的 AI 移植铺好了路。只是门槛从"一支顶尖团队干一年多"变成了"一个订阅套餐跑两周"。

那 C# 呢?

三位 C# 选手

我知道的用 C# 做这件事的项目,目前有三个,按时间顺序说。

1. typescript-csharp(作者 token,2026 年 7 月起)

三个里最早开工的。用 Claude Code 做的一比一移植,.NET 10 NativeAOT。前期主要用 Claude Fable 5 和 Opus 4.8,commit 的 Co-authored-by 里都能看到;最近换成了他自己做的开源 AI 开发工具 NextCoWork,模型是 Opus 5.5,所以新的 commit 里没有 Co-authored-by 了。它拿官方仓库的 49,321 个基线文件当标准,逐字节比对,进度表一路记得很细。

2. hez2010/TypeScript(作者 hez2010,9 月 22 日起)

用的是 200 美元一个月的 Codex 套餐,模型是 GPT-6.1 Sol。直接在官方仓库的 fork 里做,大约两周把编译器、构建、增量编译、LSP 一路做完,并且把 C# 版设成了默认编译器。

它和另外两个思路不太一样:不是逐文件翻译,而是按职责重新切分,允许多个 Go 函数合并成一个 C# 实现,C# 代码量只有 Go 版的一半左右。为了保证这样改了之后行为还一样,它把每一处和 Go 的差异都记进一本"差异台账",验证做得极其细致。本文的性能测试工具也是从这个仓库里来的。

国庆期间,公众号上已经有一篇文章详细介绍过这个项目:

评论区的画风大概是这样的:

C# 做编译器的确不行。

搞出来发现不如 Go......那也没事,毕竟也没人在意这个项目。

3. stepwisehq/typescript-dotnet(10 月 8 日起)

作者自报家门:"I'm a C# Stan."(我是 C# 铁粉)。他坦言是被 Theo 那个"40 万美元"刺激到了,回了一句 "Hold my 🍺"。README 里写的总成本:约 75--100 美元。

他用 Codex(Astra 6 做总调度,6.1 Sol 做子任务),一开始就给 AI 定了规矩:尽可能贴近 Go 源码,函数边界、类名、文件结构都保持一致。

检查器移植完之后,他才开始专门做性能。当时用它检查 VS Code 的源码,Go 版分配了约 5.1 GB 内存,C# 版分配了约 56 GB,耗时是 Go 的 6 倍多。一轮优化下来,引入了一个模仿 Go slice 语义的 Slice<T>,以及一个直接操作字节的 ByteStringView,C# 代码在长相上仍然跟 Go 很像。按 README 的说法,最后编译 VS Code(含 emit)只比 Go 慢 8.5% 左右。

他在 README 里也说了句很实在的话:地道的 Go 在内存分配和共享上本来就比地道的 C# 更高效。

跑个分

怎么测的

测试工具用的是 hez2010 仓库 typescript-cs 分支里的 csharp/tools/performance-cli.mjs(我加了个参数用来换挑战者,见文末),它的做法我很认可:

  • 成对、交替:每一轮 Go 和挑战者各跑一次,先后顺序轮换,抵消机器状态的波动。
  • 每次都是新进程 :包含进程启动时间,和你在命令行里敲 tsc 的体验一致。
  • 输出必须逐字节相同 :生成的 .js、.d.ts、诊断信息只要跟 Go 版有一个字节不同,整轮测试直接失败。所以下面每个项目,在这些用例上的输出都和官方 Go 版完全一致。
  • 3 次预热,5 组有效样本,取中位数。

测试用例是工具自带的几个合成项目:

用例 内容
version tsc --version,基本就是测启动速度
medium 64 个文件,每个 24 个接口 + 常量,编译并输出 JS 和声明文件
declarations medium 再加一些条件类型、映射类型,只输出声明文件
huge-file 单个文件,16384 个带类型标注的导出常量

所有测试都在同一台 Windows x64 台式机上完成。所有项目都是 Release + NativeAOT 编译的原生程序,C# 统一用工作站 GC。

测试日期:typescript-csharp (token) 是 2026 年 10 月 15 日,其余几个都是 10 月 20 日(终端截图也是 10 月 20 日重跑的)。这几个项目都还在快速迭代,你看到这篇文章时,数字很可能已经变了。

结果

先看相对 Go 的耗时倍数(Go = 1.00,越小越快):

项目 语言 version medium declarations huge-file
ts-rust 0.7.0 Rust 0.82 0.77 0.75 0.97
官方 tsgo Go 1.00 1.00 1.00 1.00
typescript-dotnet C# 1.11 1.29 1.30 1.86
typescript-csharp (token) C# 1.07 2.05 2.39 2.31
hez2010/TypeScript C# 1.14 3.63 2.98 3.91

再看绝对值(括号里是同一轮测试中 Go 的数据):

项目 medium 耗时 huge-file 耗时 huge-file 峰值内存
ts-rust 45 ms(Go 58) 261 ms(Go 270) 156 MB(Go 166)
typescript-dotnet 78 ms(Go 61) 535 ms(Go 287) 169 MB(Go 167)
typescript-csharp (token) 138 ms(Go 67) 683 ms(Go 296) 178 MB(Go 159)
hez2010/TypeScript 227 ms(Go 62) 1134 ms(Go 290) 241 MB(Go 172)

写到这里,我又在终端里把 typescript-dotnet 对 Go 重跑了一次,下面是原样截图(输出里的 csharp 指的就是挑战者)。每次跑数字会有几个百分点的浮动,但倍数和上表基本一致:

一句话总结:Rust 比 Go 快,C# 比 Go 慢。C# 里最快的 typescript-dotnet,在中等项目上比 Go 慢三成左右,大文件上慢不到一倍;另外两个在中等项目上是 Go 的 2--4 倍耗时。

几点说明

为了不让大家在评论区替我说出来,先自己交代清楚:

  • 这些都是很小的合成用例,几十毫秒级别。真实的大项目(比如 VS Code 的源码)我没测,结论不一定一样。比如 typescript-dotnet 作者自己测 VS Code 只慢 8.5% 左右,比上表好看不少。
  • 只有 5 组样本,几十毫秒的用例本身噪音就有一二成,名次是稳的,小数点后第二位别太当真。
  • 几个项目移植的上游 Go 版本并不完全相同,功能完整度也各不一样,这里只比较它们都能跑、并且输出一致的部分。
  • 所有 C# 版本统一用工作站 GC。hez2010 的仓库里还测过 Satori GC,大文件能再快近两成,代价是内存更高。
  • 如果你看过国庆那篇文章,可能记得"只慢 1.2--1.6 倍"这个数字。那是 hez2010 文档里另一组负载的结果;同一篇文章里引用的命令行端到端数据(64 模块 208 ms vs Go 55 ms,大文件 1100 ms vs Go 300 ms),和上表是一个量级。
  • ts-rust 的 Windows 版用的是作者发布的二进制,其它几个是我本地编译的。

所以,C# 能超过 Go 和 Rust 吗?

我知道评论区会说什么:"C# 果然不行""慢了一倍还好意思发""早就说了 Anders 选得对"。

说得都对。

你想啊,Go 那边是 100 人年的家底,加上微软一支顶尖编译器团队一年多的移植;Rust 那边,Theo 前后烧了 42 万美元。C# 这边呢?三个项目,一个总共花了一百来美元,另外两个用的就是大家手里那种每月 100、200 美元的 Claude Code 或 Codex 订阅套餐。

拿一两百美元的订阅,去超过百万美元级别投入的成果------有可能吗?

而且这三个项目现在主要走完的,是"把行为对齐"这一步:先保证每一个报错、每一个字节都跟 Go 一样。性能优化,各家都才刚刚开始。typescript-dotnet 在检查器移植完后专门做了一轮,就从这些项目里跑到了最前面。

当然,ts-rust 的第一版也比 Go 慢两三倍,后来是让 AI 专门盯着性能跑了两周才反超的。不过 Rust 嘛,大家都知道,那是 Rust。

至于 C# 做完第二步、第三步之后会怎样------我也不知道。毕竟,这当然是不可能的。

想自己跑一下?

所有项目都是开源的,测试工具也在仓库里:

原版工具只比较 Go 和 hez2010 自己的 C# 版。我在 sdcb/TypeScript 的 typescript-cs 分支上做了一个小改动:加了一个 --csharp-executable 参数,用来换成别的可执行文件(typescript-dotnet 的命令行参数不太一样,也做了个简单映射);另外加了 --ignore-input-activity,机器有人在用时也不丢弃样本。用 --go-executable 指定官方 Go 版,--csharp-executable 指定挑战者,就能得到和上面同样口径的成对数据:

powershell 复制代码
node csharp/tools/performance-cli.mjs `
  --manifest <C# 打包产物的 manifest.json> `
  --go-executable <tsgo.exe> `
  --csharp-executable <挑战者.exe> `
  --samples 5 `
  --filter '^(version|medium|declarations|huge-file)$'

如果你手上有一个更快的版本,或者在真实项目上测出了不一样的结果,欢迎在评论区贴出来。数据都在这里,欢迎打脸。

交流

感谢阅读到这里,如果感觉本文对您有帮助,请不吝评论 和点赞,这也是我持续创作的动力!

欢迎关注微信公众号 .NET骚操作,或者加入 .NET骚操作 微信交流群:

(如果微信群二维码过期,请加入 .NET骚操作交流QQ群 :495782587)