那一天下午,我的电脑风扇转了大概五分钟,刚好是npm run dev之后最熟悉的动静。
启动一个中型管理后台,Vite 5配Rollup,项目里大概1500个依赖包,首次冷启动要等十几秒,热更新碰上大组件偶尔也卡。更让人难受的是,ESLint全量检查跑一次要好几秒,TS类型检查还得另算。这些时间碎得像沙,每天都漏掉一点,但你总觉得"这就是前端开发的常态"。
直到尤雨溪带着VoidZero入场。
如果你过去一年关注过前端圈,应该已经看到不少关于Oxc、Rolldown、oxlint这些名字的技术讨论。它们的共同点是:用Rust重写JavaScript工具链的底层,从编译器、打包器、linter到类型检查,全部推翻重建。这不是给Webpack换个配置、加个缓存那么简单,而是对整个"构建与开发基础设施"的一次范式级重构。
这篇文章想聊的,不只是"VoidZero发布了什么"。我想拆开它的技术路线,搞清楚一个核心问题:为什么一个以写JavaScript框架闻名的人,最终会亲自下场去重写工具链?这场重构到底动了谁的蛋糕,又给普通前端开发者的日常带来了什么。
1. JavaScript工具链的"慢"到底慢在哪
要理解VoidZero的价值,得先理解痛。很多人以为前端构建慢是"项目太大了",但项目大只是一部分原因,更根本的问题是:我们一直在用JavaScript去解析JavaScript、用JavaScript去编译JavaScript、用JavaScript去检查JavaScript写出来的代码。
1.1 从一次"等构建"的经历说起
我手头有个业务项目,代码量不算夸张,20万行左右。开发模式下Vite冷启动大概8到12秒,这是指浏览器真正能打开页面。可一旦你改了某个被大量import的工具模块,热更新经常要两三秒,状态还偶尔丢。生产构建走Rollup,加上Terser压缩、ESLint收尾,全量下来经常逼近3分钟。
这种体验持续了一年多。同事们的普遍反应是"忍忍就过去了"。但我一直觉得不对劲:为什么写C++、Go的人从来没有这种"等构建"的体验?甚至写Python的人,虽然解释执行慢,但启动一个开发服务也就是秒级的事。
后来我意识到,根子出在工具链选型上。我们用的Webpack也好、Rollup也好、Babel也好,都是JavaScript写的。JavaScript是一门为浏览器设计的解释型语言,它本身的优势是生态大、上手快,但作为"编译其他代码"的基础设施,它的执行效率有个天然的天花板。
1.2 慢的根源:纯JavaScript实现的解析与转换链路
做个粗颗粒的对比。JavaScript解析器,比如Babel用的@babel/parser,在纯JS环境下解析一个1MB的文件,需要做大量的动态类型判断、对象属性访问、字符串操作。V8引擎再怎么优化,JIT编译也扛不住这种高频的字符级操作。
而Rust编写的解析器(比如Oxc的parser),在编译阶段就把所有这些热路径编译成了机器码,内存分配更可控,字符串处理走零拷贝策略。官方博客给过一个benchmark:Oxc的parser在解析JavaScript代码时,速度大概是Babel parser的5到10倍,在部分场景可以达到更高。
听起来只是"快了几倍",但你要知道,工具链的每个环节都是互相叠加的——解析慢、转译慢、压缩慢、lint慢,每一步都在指数级放大等待时间。ESLint的底层也是用JS写的规则引擎,跑一万条规则就要做一万次AST遍历。Oxc出品的oxlint则完全不同,它在Rust层做了一大堆并行化和规则预过滤,实测中同样体量的项目,oxlint全量检查能在几百毫秒内跑完,ESLint动辄好几秒。这不是快了一星半点,是数量级的差距。
1.3 原生语言重写的本质:把"解释型引擎"换成"编译型引擎"
有人觉得"用Rust重写"是某种炫技,其实不是。这里的本质是,把工具链从"运行时解释执行"换成了"编译期生成机器码"。
拿打包器举例。之前用Rollup,它的核心逻辑是:读取模块、递归解析依赖、生成chunk。每一步都涉及大量的AST操作和模块间数据流转。JS写出来的代码,内存模型里有大量对象引用和垃圾回收压力。Rust版本(Rolldown)则把模块图的数据结构设计得更加紧凑,利用Rust的所有权模型和并行化能力,在ARM和x86架构上都能稳定发挥。
所以VoidZero做的事情,本质上是在对过去二十年"JavaScript everywhere"的一次纠偏:JavaScript应该被用来写业务逻辑和应用逻辑,而不是用来写编译器和基础设施。那些对性能极度敏感的工具链层,交给更适合的系统语言来做,这是一条被Go、Rust生态反复验证过的路线。
2. VoidZero的技术底座:Oxc、Rolldown与原生语言路线
VoidZero不是凭空冒出来的。从公开的技术演进路线来看,它由一个叫Oxc(Oxidation Compiler)的Rust工具链项目扩展而来,针对前端开发中最耗时的几个环节逐一攻破。
2.1 尤雨溪为什么选择从编译器层入手
过去几年,很多团队都在试着优化前端构建。有人做缓存,有人做按需编译,有人做商业化CDN。尤雨溪的选择不太一样——他从编译器层开始,直接切中所有工具链的共同底座。
任何前端工具都绕不开解析。无论是Babel转译、ESLint检查、Webpack打包、还是TypeScript类型分析,第一步永远是"把源代码变成AST"。如果你能把AST生成这步做到极致,那么所有上层工具都能受益。Oxc的核心目标就是把parser、transformer、minifier、linter全部在Rust层做掉,并且让它们共享同一个编译器基础设施。
这有点像你想改造一座城市的交通,如果只加宽某一条马路,很快另一个路口又会堵。但如果你先把轨道交通的底层打通,那么整个城市的通勤效率都会上台阶。Oxc就是这个"轨道交通底网"。
2.2 Oxc:一个集解析、转换、压缩于一体的Rust工具链
Oxc的全称是Oxidation Compiler,里面包含了很多子项目:
oxc_parser:最快的JavaScript解析器之一,支持最新的ECMAScript语法和JSX、TS语法。oxc_transformer:负责做语法降级和代码转换,目标是替代Babel的一部分职能。oxc_minifier:代码压缩器,对标Terser,压缩速度和效果都有明显提升。oxc_linter:也就是oxlint,对标ESLint。oxc_resolver:解决Node模块解析算法,对标enhanced-resolve。
这套设计最聪明的地方在于模块化。你不用一次性全量替换,可以先用oxlint替换ESLint,感受一下速度;也可以先在构建流程中引入oxc_parser替换掉原来的parser,看看有没有兼容问题。它是一把把换上去的手术刀,而不是直接要求你把整个手术台都换了。
2.3 Rolldown:用Rust重写Rollup,兼容插件生态
Rolldown是VoidZero项目在打包器层面的核心产品。它的目标不是造一个全新的打包器,而是做"Rust版的Rollup"。
为什么要兼容Rollup而不是另起炉灶?因为Rollup的插件生态太成熟了。社区里大量现成的插件都是围绕Rollup API写的,比如@rollup/plugin-commonjs、@rollup/plugin-node-resolve、@rollup/plugin-terser。如果Rolldown不兼容这套API,所有插件都要重写,那迁移成本高到没人愿意用。
Rolldown在兼容Rollup插件接口的同时,把底层模块图构建、代码生成、tree-shaking算法全部用Rust重新实现。这就使得它既能无缝接管现有Vite项目,又能在性能和构建质量上实现数量级的提升。
2.4 Oxidation Compiler的模块化设计:每个环节都可以独立使用
很多人在看VoidZero的时候,容易把它理解成一个"大而全"的框架,比如"要全套接入才行"。实际上恰恰相反,Oxc和Rolldown的设计都刻意保持了独立性和可插拔性。
举个例子,我可以先只使用oxlint来替换ESLint做代码检查。这个替换不需要改动构建链路,也不需要改代码。跑一遍之后,如果发现某些规则的不一致,还可以通过配置逐步对齐。之后再把项目里的Vite升级到新版本,让Rolldown接管打包,这两个替换动作互不干扰。
这种"渐进式替代"的策略很务实。它承认老项目不可能一个月内重构完,所以给了一条平滑的过渡路径。让工具链的升级不再是一刀切的推倒重来,而是像换轮胎一样,一个一个地换,换到哪个都能立刻开。
3. 范式级的重构:从"更快"到"更简单"
速度只是表面现象。VoidZero真正想做的事情,是通过统一基础设施,让前端开发的复杂度降下来。
3.1 从"各自为政"到"一条流水线":一次解析、多方受益
我最烦的一件事,是同一份代码在工具链里被反复解析。Babel解析一遍做转译,ESLint再解析一遍做检查,Terser又解析一遍做压缩,Rollup还要解析一遍做打包。同一个AST,每个工具都用自己的方式生成一遍,既浪费CPU,又容易出现"每个工具理解不一样"导致的结果偏差。
Oxc的设计目标是让这些环节共享同一个AST和同一套编译基础设施。理论上,一次解析的结果可以被转译、lint、压缩、打包共同复用,整个链路只需解析一次。这就像一座工厂里,同一个原材料进门之后,所有工序都在一条流水线上完成,而不是每个车间都派人到门口把原材料重新搬一遍。
实际落地到工程上,带来的提升非常直观:冷启动时间大幅缩短,热更新更快,构建更稳定。而且由于各环节使用同一个编译器,某些"每个工具理解不一样"导致的隐性bug也会变少。
3.2 Vite与Rolldown的结合:开发态与构建态的鸿沟被填平
Vite 5时代一个常年被吐槽的问题是:开发环境和生产构建行为不一致。
开发模式下,Vite用esbuild做依赖预构建,用原生ESM跑源码;生产模式下,Rollup做打包,再进行压缩和代码分割。因为底层不一样,同一个组件在开发环境表现正常,一到生产构建就报一个奇怪的"循环依赖"错误。这种割裂感让老前端非常难受。
Rolldown接入Vite之后,开发态和构建态可以统一到同一套Rust打包引擎上。你在开发环境看到的热更新行为和最终构建产物的行为,底层逻辑是一致的。这种一致性带来的不只是性能提升,更重要的是心智模型的简化——你不用再为"开发环境正常、构建报错"这种玄学问题浪费半天时间排查了。
3.3 Lint、类型检查、打包、压缩:四周围生态的整体升级
VoidZero显然不满足于只解决打包器的问题。它把触手伸到了前端工具链的各个痛点上:
| 工具类型 | 现有主流方案 | VoidZero方案 | 核心优势 |
|---|---|---|---|
| 解析/转译 | Babel | Oxc Transformer | 解析速度提升明显,AST共享 |
| Lint | ESLint | Oxlint | 速度数十倍提升,规则兼容度好 |
| 打包 | Rollup/Webpack | Rolldown | Rollup插件兼容,Rust底层 |
| 压缩 | Terser | Oxc Minifier | 压缩速度更快,压缩率相近 |
| 类型检查 | tsc | Oxc/solatedDeclarations等 | 提供部分类型的加速方案 |
注意,上面的"类型检查"一行,VoidZero没有直接说"完全替代tsc",因为TS类型检查是个极其复杂的系统,短期内不可能全部重写。但它在逐步提供更快的路径。比如通过Oxc在语法层面快速发现类型错误,把tsc做成兜底,让开发者在绝大部分场景下获得"秒级反馈"。
3.4 Rust工具链的互操作:N-API/FFI周转,以及对JS的降级兼容策略
前端工具要让普通开发者用上Rust能力,中间必须有一层桥接。类似esbuild那样通过Go编译成二进制,再通过child_process调用;Rust方面则通常通过N-API(Node-API)来暴露接口。
VoidZero的做法是利用N-API把Rust层的解析、转换、lint能力暴露给Node.js调用。这样开发者依然是在Node生态里写代码,但底下跑的是Rust实现。遇到不支持的环境或需要调试的场景,它也有降级策略,确保基本功能可用。这种"外层兼容、内层重构"的思路,最大限度地降低了使用门槛。
4. 老项目怎么落地:迁移路径与踩坑记录
说了这么多原理,你可能更关心:老项目能不能迁?迁起来痛不痛?我拿自己负责的一个Vite+React项目,以及另一个Webpack老项目,分别做了迁移试验,踩了一些坑,分享一下实际路径。
4.1 用oxlint替换ESLint:最安全的第一步
我的建议是,如果你还没做好全面切换的准备,先从oxlint入手。它是所有组件里迁移成本最低的一个。
操作上,先在package.json里加个脚本:
{ "scripts": { "lint:oxlint": "oxlint ." } }以一个中型项目为例,原先ESLint跑全量检查约6到8秒;oxlint首次运行约1到2秒,冷启动之后甚至可以控制在0.5秒以内。区别肉眼可见。
不过要注意几个坑:
- 部分ESLint规则在oxlint里支持度还不是100%,比如某些需要类型信息的规则,可能需要配合
typescript-eslint正在进行中的支持。 - 如果项目里自定义了很多"本地私有规则",要把它们转成oxlint的插件格式,官方文档里有对应指南。
- 建议先在CI里同时跑ESLint和oxlint,对比一段时间,确认规则覆盖和错误报告一致后,再逐步切换。
4.2 从Vite 5升级到适配Rolldown的版本:注意依赖兼容
如果你用的是Vite 5,想升级到带Rolldown的版本(比如社区实验性的rolldown-vite),整体流程比想象中顺。它尽量做到"Drop-in Replacement"——原来怎么配置Rollup,现在就怎么配置Rolldown。
我踩到的坑主要有:
- 插件兼容性:大部分Rollup插件能直接工作,但个别依赖Rollup内部未公开API的插件会失效。好在这种插件数量极少,一般都能找到替代品。
- 自定义chunk文件名策略:Rolldown对chunk命名的部分细节和Rollup不完全一致,如果你的CI里有快照测试或者严格的产物校验,要先跑一遍对比。
- 循环依赖处理:Rolldown对循环依赖的检测更严格,原来Rollup不报错但实际有问题的代码,可能会在升级后暴露出来。这不能算坏消息,但会带来临时的工作量。
建议先在一个独立分支做升级验证,用生产构建产物和旧版做对比,确认资源体积、懒加载行为、动态导入路径都没问题再合并。
4.3 渐进式接入Oxc的解析/压缩:不必一次性全换
有些项目还在用Webpack,升级Vite本来就是一个大工程。这种情况下,你可以考虑在现有Webpack流程里局部接入Oxc能力。
比如用oxc_loader替换掉项目中的部分babel-loader职责(或者通过@oxc/transformer做转译)。又比如压缩阶段,把terser-webpack-plugin换成基于oxc-minifier的插件。这种小步替换的收益非常直接,改动范围却很小。
我做过一次测试:只是把压缩器从Terser换成oxc-minifier,在保证压缩率基本不变的前提下,生产构建的总时间缩短了大约20%到30%。这个时间省下来,整个发布流程都更从容。
4.4 Webpack老项目的迁移判断:什么时候值得动
很多人问Webpack老项目到底要不要迁。我的判断标准很简单:看你是不是每天都被构建时间或热更新效率困扰。如果项目常年稳定、构建时间可接受,那完全不必赶时髦;如果组里已经有人因为构建慢而抱怨了好几个月,那就值得花时间评估。
迁移路线建议分三步走:
- 先在前端不感知的情况下,用oxlint替换ESLint,立刻获得"快"的体验;
- 再用roadmap里成熟的方案把开发服务器或构建器换成兼容层工具;
- 最后再考虑全量迁移到Vite/Rolldown,让开发态和构建态走向统一。
这个过程不用一步到位。工具链重构是一场马拉松,不是百米冲刺。
5. 新范式对前端开发者的真实影响
聊完怎么迁,再看这波重构对"人"的影响。工具链的演变,从来不只是性能数字的冷冰冰迭代。它会反过来改变我们写代码的方式、排查问题的方式,甚至能力模型。
5.1 从"工程优化"到"底层重构":工具链定位的根本转变
过去几年,前端性能优化更多是"工程层面"的——拆包、缓存、并行化、SSR、边缘渲染。这些都是好手段,但都是在既有工具链框架内做文章。
VoidZero带来的最大信号是:不把工具链当成"配置就完事儿"的黑盒,而是把它当成可以重写、可以自控的底层设施。这让"工具链性能"和"框架运行时性能"站在了同等重要的位置上。未来前端基建的边界,会被进一步向上推。
往小了说,这是尤雨溪个人职业生涯的一次大胆转型;往大了说,这是一个典型的技术信号——编译器和系统级能力,正在重新成为前端圈的核心竞争力。
5.2 前端工程师的能力模型:了解编译器底层不再是加分项
以前,写前端可以完全不懂Babel的插件原理、不懂AST、不懂Rust,依然能写出很好的业务代码。但现在我会劝身边刚入行的同事:如果时间允许,去了解一点编译原理的基础知识,去上手写一个最小的AST插件。
这不代表所有人都要去学Rust。但你需要理解Rust工具链和纯JS工具链的差异,理解AST生成的代价,理解"为什么同一件事用系统语言写快了10倍"。这些底层认知会直接影响你在选型时的判断力,不然就只能人云亦云地跟着框架走。
5.3 我对这套新工具链的实操体会
最后聊点个人感受。
我最近已经把主力项目的lint全部切到了oxlint,编辑器保存时的"即时反馈"让人心情愉快;生产构建也在一部分项目里试用了Rolldown,构建时间肉眼可见地缩短。但我仍然保留ESLint用于生产前的最终校验,因为部分自定义规则的兼容性还没到100%。
我的建议是:如果你是个人开发者或者小团队,直接上最新工具链完全没有问题,收益立竿见影;如果你在一个有着大量历史包袱的大型项目里,不要着急,先把工具链的切换当成一个季度级别的专项来做,逐步替换,每个环节独立验证。
工具链的重构浪潮才刚刚开始。现在入场,你不需要背任何历史包袱,可以直接生在一个"更快、更简单"的开发环境里。而站在我这种经历过Webpack慢启动时代的人的角度看,这种感觉有点像突然换了一台新电脑——明明还是写着同样的代码,但整个人都轻快了不少。