根据问题类型选择语言:TypeScript Go 版本移植的经验教训
选择 Go 作为原生 TypeScript 编译器这一做法,体现了如何根据工作负载挑选合适的工具、重视工具的运行速度,以及如何逐步迁移大型代码库。
在大型 TypeScript 单体仓库中等待自动补全功能出现近一分钟,这是开发者们常见的困扰;移动端开发人员在 Xcode 对大型项目进行索引时也会遇到同样的问题。TypeScript 团队正是为了解决这一问题,他们所采用的解决方式为我们提供了一个很好的案例,展示了如何基于事实而非潮流来做出技术决策。本文将超越那些基准测试的标题,提炼出那些你可以应用到自己工具和应用程序中的工程思路,无论你使用的是 TypeScript、Swift 还是两者兼用。
微软宣布了什么
2025 年 3月,创造了 TypeScript 和 C# 的 Anders Hejlsberg 宣布,微软正在为 TypeScript 编译器及其语言工具开发原生版本。许多人以为目标语言会是 Rust 或 C++,但实际上是 Go。
代号为 Corsa 的原生实现是 TypeScript 7.0 的基础。截至本文撰写时,预计发布时间在 2026 年中期左右,因此在规划迁移之前请查看 TypeScript 官方博客以了解最新进展。
相关测试数据显示提升十分显著:在编辑器中加载 VS Code 代码库的时间从大约 9.6 秒缩短至 1.2 秒,内存使用量也减少约一半;微软称整体项目加载速度提升了约 8 倍,类型检查速度则提升了高达 10 倍。如果您想了解这些改进在实际项目中的具体表现,我们已在 无需修改代码即可实现的 Go 重写优势一文中进行了详细阐述。本文的其余部分将重点讨论做出这一选择的理由。
为何选择 Go 而非 Rust
该团队正在寻找一种最低层级的语言,它不仅能在TypeScript需要支持的每个平台上生成原生代码,还具备良好的内置并发支持。Go正好符合这些要求。
还有两个重要因素。首先是对工作量的考虑:对大型项目进行类型检查,很大程度上就是并行检查多个文件后再合并结果,而goroutine恰好能直接对应这种模式。其次是与现有代码的兼容性:该编译器基于庞大的JavaScript代码库,依赖垃圾回收机制,并采用较为函数式的编程风格。Go同样拥有垃圾回收器及类似的架构,因此可以很紧密地转换现有代码。若将其改写为Rust,则必须在所有权和生命周期方面进行大幅重构,对于拥有庞大代码库的团队而言成本极高。
简而言之,该团队并未选择最时髦的语言,而是选择了与自身并发模型相匹配、且能提升工程师工作效率的语言。这是一种架构层面的决策,就像在决定为某个功能使用 UIKit 还是 SwiftUI,或是 Combine 还是普通的 async/await 时所做的决策一样。
Swift 中的相同思路
Swift 开发者之前也曾遇到过类似问题。结构化并发正是为了解决在不牺牲正确性的前提下并行执行耗时任务的问题而加入 Swift 的。下面的代码片段通过任务组实现了“同时检查多个文件,然后合并结果”的思路:每个文件都有对应的子任务,父任务则在各子任务完成后收集相关诊断信息。需要注意的是,尽管这段代码展示了 Go 编译器使用 goroutine 的模式,但它仍是用 Swift 编写的。
// Swift concurrency: parallel "type-checking" of files, similar in spirit
// to how Go's goroutines let TypeScript check many files at once
func checkFiles(_ paths: [String]) async -> [Diagnostic] {
await withTaskGroup(of: [Diagnostic].self) { group in
for path in paths {
group.addTask {
await checkSingleFile(path) // runs concurrently
}
}
var results: [Diagnostic] = []
for await diagnostics in group {
results.append(contentsOf: diagnostics)
}
return results
}
}
有几点值得注意。withTaskGroup能确保每个子任务都在函数返回前完成,从而避免有工作遗漏在作用域之外。结果会按照完成顺序而非输入顺序返回,因此如果诊断结果的顺序很重要,就需要在之后对它们进行排序。而并行性之所以安全,是因为每个检查都是独立的,这正是类型检查非常适合Go编程模型的原因。
如果你已经使用了Swift 6,那就已经做出了这样的权衡:你提前付出成本以满足严格的并发性和安全性检查,而作为回报则是更快的编译速度和更稳定的运行时表现。微软在其编译器中也做出了类似的抉择。
可应用到你自己项目中的经验
- 工具的运行速度是产品的重要特性。缓慢的构建速度和迟缓的自动补全功能,会给团队中的每位开发者带来日复一日的、几乎难以察觉的负担。针对构建流程、编辑器和索引系统的性能优化,与针对已发布应用的性能优化具有同等重要性。
- 应让问题而非趋势来决定工具的选择。无论你选择 MVVM、SwiftData 还是 Combine,正确的方案都应基于数据在系统中的实际流动方式来确定,而非依据本月社交媒体上的流行趋势。
- 重构可以逐步进行。TypeScript 团队尽可能忠实地移植了现有的编译器,而非重新设计它,因此新版本能产生与旧版本相同的结果。这充分说明了没有必要在单次迭代中就彻底重建整个应用架构。
什么时候不应遵循这个示例?如果当前的性能瓶颈并非所在技术栈,采用移植方案只会带来风险。请先进行测量,确认真正慢的部分确实位于你打算替换的层中。
识别警示信号
导致这个问题的症状在规模日益扩大的任何代码库中都可能出现:类型推断变慢、编辑器反馈延迟,以及构建流程图不断膨胀。解决之道并不总是更换新框架,有时需要仔细查看工具在底层的具体运作方式,并修改那些承担了过多工作的部分。
关键要点
- 选择 Go 是因为它的原生编译、垃圾回收机制以及基于 goroutine 的并发模型,既适合处理类型检查的工作负载,也契合现有代码的结构。
相关阅读
- 现代JavaScript的真正复杂性源自工具而非语言本身 —— 本文阐述了async/await和可选链等核心JavaScript特性如何简化代码,而过多的工具和依赖却会带来不必要的复杂性。
- 深入了解 TypeScript 7 的 Go 重写机制:无需修改代码即可提升速度 — 了解 TypeScript 7 基于 Go 的编译器如何实现 8-12 倍更快的构建速度,这种架构变革为何有效,以及如何安全地升级现有项目。
- 从单一设计到多屏展示:更合理的启动画面资源处理流程 — 为何启动画面的制作时间远超其设计所需,如何创建能适配各种屏幕比例的启动画面,以及基于浏览器的专用生成工具应具备哪些功能。
- TypeScript 7升级决策:并行检查器、监视模式与API缺陷 — 从TypeScript 6升级到原生支持Go的TypeScript 7后会带来哪些变化,新的并行性标志如何运作,以及哪些工具链应在升级前暂缓使用。
- 从编写Kotlin到指挥智能体:移动工程师的新角色 — 编码智能体如何让Android工程师的工作重心转向需求规范、上下文环境、架构约束及验证,以及哪些基础技能比以往更为重要。