工具链的第二次出生

升级 Vite 8 那天我盯着终端看了好几秒。47 秒变成 6 秒。

第一个反应不是高兴,是怀疑。缓存没清吧?插件没加载吧?产物是不是少了什么?我把 dist 目录翻了一遍,该有的都在,文件名里的 hash 也对得上。

就是快了。

这种不敢相信的感觉挺有意思。我们这行在过去十年里被训练得相信一件事:构建慢是正常的,项目越大越慢是物理规律,你能做的只有加缓存、加机器、把 CI 拆成更多 job。突然有人告诉你这条物理规律不成立,你的第一反应不是接受,是怀疑自己看漏了什么。

停下来的传送带旁边放着一只秒表

# 这一年到底发生了什么

如果你最近没盯着工具链,大概低估了这一年的变化幅度。

Vite 8 在三月发布,底层的 esbuild + Rollup 双引擎换成了用 Rust 写的 Rolldown。以前开发时 esbuild 负责依赖预打包、生产时 Rollup 负责出包,两套东西对同一份代码的理解一直有细微差别——那些「dev 能跑 build 就挂」的玄学问题,有一半是这么来的。现在只剩一个引擎。

TypeScript 7 在七月 GA。编译器核心从 TypeScript 本身移植到了 Go,官方给的数字是 8 到 12 倍。VS Code 那个仓库的类型检查构建从 78 秒掉到 7.5 秒。

Oxc 在做解析器、linter、formatter、minifier 的全家桶,Biome 在吃 Prettier 和 ESLint 的份额,Rspack 在接 webpack 的位置。进度各不相同,方向是一致的:原本用 JavaScript 和 TypeScript 写的工具,正在被用系统语言重写一遍。

# 流行的解释漏了最关键的一半

关于这件事,社区最常听到的说法是「Rust 快」。

这个说法不算错,但它讲的是结果,不是原因。如果只是因为快,为什么是这两年,而不是五年前?esbuild 2020 年就出来了,当年也快得吓人,可主流项目该用 webpack 还是用 webpack。

真正变了的其实是另一件事:终于有人愿意为工具链付账了。

写工具的人一直是前端生态里最吃亏的一群。用户几百万,赞助几百块。你要在业余时间维护一个被上百万人依赖的解析器,还得应付每个月新冒出来的语法提案。在这种条件下,一件工具只要「能用」,就没人有动力把它推倒重来——重写意味着几年没有产出,而这几年的收益还不属于你。

Vite、Rspack、Oxc 背后都有公司和商业支撑。这不是巧合。

# 微软为什么选了 Go

这是整件事里我最想讲的一段。

TypeScript 团队决定做原生移植的时候,几乎所有人都默认他们会用 Rust。毕竟 Rust 是这几年系统语言的新宠,Rolldown、Oxc、Biome、Rspack 清一色 Rust——快、内存安全、没有 GC 停顿。

他们选了 Go。

官方给的理由朴素得有点扫兴:Go 的编程模型和现有 TypeScript 代码库的结构更接近。TypeScript 编译器是一个高度面向对象的代码库,对象之间互相引用,到处是共享可变状态,依赖垃圾回收。这种代码搬到 Go 里,很多地方可以近乎一比一地翻译。搬到 Rust 里,你得先想清楚每个对象的所有权归谁、生命周期多长——那不叫移植,那叫重新设计。

而 TypeScript 恰恰不是一份能重新设计的东西。这个编译器里堆了大约 100 人年的工作量,绝大部分不是算法,是十几年攒下来的兼容性判断:这种写法在 2014 年的某个版本里被允许过,有人依赖它,所以现在还得允许;那种推断在某个边界条件下会退化成 any,改了会炸掉一批库。这些东西没有文档,只活在实现里。

要把这种东西搬到另一门语言,最重要的性能指标不是运行时速度,是「一比一搬过去的难度」。Go 在这个指标上赢了 Rust。

两块路牌指向两个不同的方向

# 移植和重写是两件完全不同的事

我想把这条线单独拎出来,因为它解释了不少看起来矛盾的现象。

esbuild、Rolldown、Oxc 是从零开始设计的。目标很明确:吃掉 JS/TS 的一个子集,在这个子集上做到最快。语法支持不完整可以慢慢补,边界情况可以先不管。它们有资格选 Rust,因为没有历史包袱需要一比一保留。

TypeScript 没有这个资格。它的全部价值就在那些边界情况里。一个「支持 95% 语法、快 50 倍」的 TypeScript 是没有意义的,因为那 5% 里全是别人的生产代码。

所以它只能选一门能让它把包袱一起带走的语言,哪怕那门语言理论上没那么快。

选语言这件事,从来不是选语言本身,是选你能带走多少过去。

一摞图纸从一张桌子搬到旁边另一张桌子,一张都没有重画

# 慢工具会替你做决定

说完实现,说点跟日常更有关的。

我一直觉得工具的性能被低估了,因为它省下的不只是时间,它还会改变你工作的形状。

类型检查要跑 60 秒的时候,你不会把它挂在保存时自动执行。你会把它挪到 CI,挪到 pre-push,最后挪到「上线前想起来就跑一下」。lint 慢的时候你会关掉几条规则,因为「这条规则不太值」。构建慢的时候你会倾向于把改动攒成一堆再统一验证,而不是改一点验一点。

这些决定每一个单独看都合理。加在一起,它们把你的节奏往「大批量、低频次、延迟反馈」的方向推。你以为这是你的工作习惯,其实有一部分是你工具的延迟在替你决定。

当类型检查从 60 秒变成 5 秒,发生的不只是省了 55 秒。它被挂回保存时自动执行了,错误在你写完下一行之前就冒出来,你开始敢在重构的时候大范围改类型。这些都是慢工具时代根本不会进入选项的做法。

# 我并不确定这全是好事

写到这儿该泼点冷水。

这一轮重写有个很少被提的代价:前端工程师对自己工具链的可读性在下降。

十年前你用的工具基本都是 JS 写的。Babel 插件你会写,webpack 配置你读得懂,遇到诡异行为能 debugger 进源码去看。现在你项目里跑的东西,大多是 Rust 或 Go 写的二进制。它对你来说是个黑盒——快,但你打不开。

一个人站在打开的机器外壳前,里面是他看不懂的结构

这不一定坏。我不认为每个用编译器的人都需要读懂编译器,我读不懂 V8,照样写 JS。但这里有个区别:V8 从来就是黑盒,我们从来没有拥有过它;而 Babel 和 webpack 是我们自己的东西,是我们生态的一部分,是用我们的语言写的。这个所有感没了,是有点可惜的。

还有个更实际的问题:谁来维护。旧的 JS 工具靠社区,新的原生工具靠公司。公司会撤资,会调整战略,会把团队挪去别的项目。Oxc、Rolldown、Rspack 现在状态都不错,但把整个生态的基础设施压在少数几家公司的预算上,风险不在今天,在五年后。

# 结尾

我不怀念等 webpack 编译的那两分钟。真的,一点都不怀念。

但我确实有点怀念那种「这个项目我现在看不懂,但我可以花一个下午看懂它」的感觉。以前遇到工具的诡异行为,我的默认动作是打开 node_modules 翻源码,而且经常能翻到答案。现在我的默认动作是去提 issue。

工具链变快了,也变远了。

这大概就是进步的常态——你拿到一样东西,同时交出另一样。只是这次交出去的是「随时能看懂」,而它值不值那个 47 秒到 6 秒的差价,取决于你接下来这几年打算怎么用它。


题图与部分配图来自开放授权的摄影作品,感谢这些作者:Tool rack — 1lenore;Stopwatch test — casey.marshall;Crossroads: Invest or Spend — ccPixs.com;Gears gears cogs bits n pieces — Elsie esq.(均为 CC BY 2.0,Flickr)。