Vite 8.0 发布之后,我第一件事不是逛 Release Notes,而是赶紧拿了一个 Vite 7 的老项目试升级。标题说“2.0 以来的最大更新”,我开始还觉得是营销话术,升级完发现这次是真的把底子换掉了:构建器几乎全部移到 Rust 这边,插件 API 虽然还叫那个名字,但其实内部已经换了一轮。这篇我就带你拆一遍:为什么说它是自 Vite 2.0 以来最大的一次重构、哪些改动直接影响你日常写代码、以及从老版本升级时那些文档里不会写、但我实际踩过的坑。适合所有用 Vite 的团队和个人开发者看,不管你是只写过 demo 还是维护着几十个插件,都能从里面找到你需要关心的东西。
1. 先把背景盘明白:到底哪里“最大”了
1.1 回看 Vite 2.0 当时做了什么
要说“8.0 是 2.0 以来最大更新”,咱得先回顾一下 2.0 当年为什么能被叫“大版本”。Vite 2.0 最大的贡献是把“开发用原生 ESM、构建用 Rollup”这套双轨架构固定下来,同时正式确定了插件 API 的形态。它解决了 Vite 1 时代只能做开发服务器、没法直接用于生产构建的尴尬。可以说没有 2.0 就没有今天我能在五秒内起一个 React 项目的体验。
从 2.0 到现在,Vite 中间经过了 3、4、5、6、7 这几个版本。说实话,中间每个版本都有不少功能,但本质上还是在“开发服务器 + Rollup 打包”这个框架里做优化:缓存策略改了、依赖预构建出了个新的、SSR 好用了点、Plugin API 加了些钩子,但内核没有发生“换引擎”级别的变化。这也是很多人到了 Vite 5、6 之后感觉速度提升不像早期那么明显的原因——该优化的大头都被前面几版吃掉,剩下的只能在这里抠一点那里抠一点。
1.2 8.0 的核心:换引擎,而不是叠功能
Vite 8.0 这次的最大更新点就是:它终于把“开发跑原生 ESM、构建跑 Rollup”这个伴随了 Vite 半辈子的双轨架构,收敛成了一条用 Rust 写的主链路。构建、转换、依赖扫描、代码压缩这些过往分散在 JS / 原生模块里的工作,现在统一交给一个底层引擎处理。
这个方向其实不是冒出来的。Vite 生态里早就有 Oxc、Rolldown 这些 Rust 项目在推进,只是以前它们是“试验品”,你可以单独装个rolldown-vite体验一下,但生产环境没人敢直接上。8.0 把它们从“实验入口”正式扶正成了默认实现,这跟当年从 Vite 1 跳到 Vite 2 的选择是一样的,都是“换地基”式的改动。明白了这一点,你就知道 8.0 注定不是一个“加几个新配置项”的小版本,而是一个需要整个插件生态跟着重新适配的大版本。
2. 底层重构拆解:Rust 引擎到底改了哪些关键环节
2.1 Rolldown 全面接管打包路径
8.0 最显眼的底层变化,就是打包器默认从 Rollup 换成了 Rolldown。Rollup 是 Vite 这些年一直依赖的 JS 打包器,它的模块处理机制成熟、插件生态也非常完整,但瓶颈也很清楚:纯 JS 实现,在大型依赖图上的解析、合并、代码生成阶段耗时很厉害。Rolldown 做的事情就是把 Rollup 的 API 和插件模型在 Rust 里重新实现一遍,同时尽可能保持“你的 Vite 配置、插件代码不用大改”这一承诺。
在实际升级后,能感受到的最直接变化就是:依赖预构建时间从一个量级掉到了另一个量级。过去你项目里有几百个 npm 包,冷启动第一次要等那个optimizeDeps跑完,如果机器一般、依赖又多,经常是十几秒到半分钟。在 8.0 里,Rolldown 做这件事基本是毫秒级扫描、秒级构建,而且它是并行起多个线程去处理,你再看到命令行里卡在“预构建”那个位置的概率会小很多。
这里要特别说一句:Rolldown 不是简单把 Rollup 的源码翻译成 Rust,它的模块图模型也做了调整。它把模块解析、转换、打包阶段的中间结果都以更高效的数据结构存下来,所以不只是第一次快,增量构建和热更新时“算哪几个模块要重跑”这件事也快了很多。简单类比就是,以前修改一个文件,Vite 需要在浏览器和 Node 之间来回传几轮数据、反复跳过依赖图里那些不必要的边;现在它可以直接在 Rust 层面判断哪些节点真的变了,然后只输出那一小片补丁。
2.2 Oxc 系工具包办解析、转换与压缩
除了 Rolldown,8.0 还默认使用了 Oxc 系的原生工具。Oxc 是一个 Rust 写的 JavaScript 工具链集合,包含解析器、AST、Transformer、压缩器等多个组件。在 8.0 之前,Vite 的 esbuild 和 Rollup 组合各干各的活:Esbuild 用来转换 TS 和压缩一部分代码,Rollup 负责整体打包。但这会带来一个问题——ESM 和 CJS 之间、开发和生产之间会经过两条不同的解析链路,行为上难免有细微差异。
到了 8.0,依赖解析、TS/JSX 转换、语法降级和压缩这几件事大部分都统一到 Oxc 这条链路上了。这意味着你在开发阶段看到的语法兼容性,和生产构建拿到的结果会高度一致,不会再出现“开发正常、build 出来报错”那种双轨分叉问题。对用了一些比较新的 ECMAScript 特性的团队来说,这种一致性价值很大。
不过这里也得提醒一句:Oxc 在语法转换方面虽然有很高覆盖率,但它毕竟不是 Babel,所以如果你用了特别冷门的 Babel 插件,比如某些还在提案阶段的装饰器语法方案,8.0 不会自动帮你做那层转换。升级之前最好把你babel.config里挂的插件清单翻出来逐一过一遍,确认哪些是“交给 Vite 就行”,哪些还是要显式接入 Babel 插件链。
2.3 开发服务器与构建阶段“同一套引擎”带来的连锁反应
把开发服务器也从“原生 ESM”改成走同一套 Rust 引擎,是这次改动中风险最大但收益也很明显的一步。以前的架构里,开发模式为了让浏览器直接跑 ESM,Vite 只做了很轻量的转换,遇到非 ESM 依赖就预构建一下,基本不主动做太多重写。而生产环境要兼容老浏览器,得做完整压缩、转换为旧语法。两个路径的选项、甚至语法支持层面都不同,插件作者需要同时在两套逻辑里做兼容,非常痛苦。
8.0 在开发与构建之间大幅拉齐了处理流程。开发时虽然还是给浏览器交付 ESM,但转换、依赖分析和代码生成已经走到了同一条 Rust 管线里。这让一些原本只在构建阶段生效的插件钩子,在开发模式里也能用了,生态适配的成本也随之降低。对我们使用者来说最直观的感受是:开发模式启动更快、内存占用更稳,而且一些以前只在 build 时才暴露的奇怪错误,开发阶段就能提前看到。
3. 除了变快,还有哪些手感级的变化
3.1 模块级持久化缓存:不再是玄学
老版本里 Vite 的缓存主要靠node_modules/.vite这个目录,它做的是依赖预构建结果缓存。但应用源码本身的转换结果,在开发模式下很多是存在内存里,重启开发服务器就要重新转换一遍。代码量小的项目感受不明显,要是碰上单体仓库,几十个包打完补丁之后每天首要任务就是等冷启动。
8.0 把源码级的转换结果也改成磁盘持久化了。它会给每个模块算一个内容哈希,加上依赖关系、配置项等上下文信息,如果什么都没变,重启之后直接拿缓存结果,跳过大段重复转换。我实测下来,重启开发服务器的冷启动速度在某些项目上能快 40% 到 60%,而且这还是一个几乎不改配置就能白拿的收益。
这里分享一个使用细节:Vite 8.0 的持久缓存目录跟以前的缓存目录不冲突,升级后老目录会被自动清理或重建,所以不用手动删node_modules底下的旧缓存。但如果你给 CI 配了缓存目录路径,最好检查一下 CI 配置,别把旧的.vite缓存文件继续传上去,否则可能反而拖慢安装速度。
3.2 启动速度与内存占用:从“能忍”到“不怎么感知”
我把一个约 200 个页面、300 多个直接依赖的老后台项目从 Vite 6 升到 8.0,做了个粗糙的对比。冷启动大约从 6.8 秒降到 2.1 秒,热更新在改动多个文件时从原来的几百毫秒到一两秒抖动,降到基本稳定在百毫秒上下。内存占用方面,Node 进程的 RSS 下降了 20% 左右,因为很多临时 AST 和模块图结构都放在 Rust 那边管理,JS 堆的压力小了。
不过我也要泼一盆冷水:如果你用的是 Vite 5、6 基础配置,项目不大,模块数量也就几百个,那启动速度的体感提升可能没那么夸张。引擎再快也得先把项目结构读一遍,局域网 + 高延迟磁盘的场景对最终体感影响其实更大。真要说最大受益者,是 monorepo 里那种一个模块树包含成千上万个文件的项目。
3.3 编辑器联动与错误提示:对日常开发真正的“隐形翻新”
8.0 在开发模式下对源码做了更细粒度的 Source Map 输出,配合编辑器插件能给出更准确的“定位到报错文件”体验。以前有些 Vite 项目里报错会跳到vite:transform的虚拟文件或者一个编译后的 chunk 文件里,你点进去看到一坨压缩后的代码,根本没法定位。8.0 的 Source Map 默认解析到原始列级别,VS Code 里 Ctrl+点击可以更准确地跳到正确的行和列。
错误提示本身也换了新样式。构建失败时,它会像 ESLint 一样把报错的代码片段直接打印在终端里,并高亮出具体是哪一行哪一列出错。这个变化对排查插件的错很有帮助,因为你不必打开浏览器 DevTools 就能先在终端里看到“这个模块在哪个文件、哪个插件步骤上炸了”。老版本那种“报错在模块内部、只在浏览器 Console 里有一行堆栈”的难受体验,这次算是解决了。
4. 插件系统 API 变了:所有插件作者都得看
4.1 插件 API 兼容性:看起来一样,实际上有坑
Vite 8.0 发布时主推的一个卖点是“插件 API 尽量保持兼容”。说实话,这个“尽量”很微妙。绝大多数旧插件不用改就能在 8.0 里引用成功,但如果你用到了某些内部钩子的返回格式或执行顺序,就会在实际运行时报一些很莫名的错,比如“Returned value is not a valid chunk”之类的。
我升级后第一个碰到的问题就出在一个用enforce: 'pre'的插件上,它做的是把第三方依赖里的某些字符串替换掉。在 Vite 5 时代,这个插件在transform钩子里拿到的模块代码已经经过了前一轮 esbuild 转换,所以代码里很多内容是“展开”的。到了 8.0,因为转换链路换成了 Oxc,同样一个钩子里拿到的源码形态不同,正则匹配自然就失效了。这个问题的排查思路是:不要假定钩子拿到的代码是“已经被某工具格式化过”的,最好用 AST 或更语义化的方式匹配目标内容。
4.2 钩子运行机制的三个关键变化
插件作者最应该知道的三个变化,我总结一下:
resolveId钩子从同步为主,变成大量支持并行异步。也就是说 Vite 会同时发起多个模块解析请求,插件里如果用了共享状态或者全局变量,很容易出现并发覆盖。写插件最好把这些状态改成局部变量,或者用Map并以importee作为 key。load钩子的返回值现在会被当作不可变数据。以前有些插件会把返回的字符串再做String.replace改一改,但在 8.0 里如果返回之后觉得不对,再继续 mutate 可能不生效,因为你拿到的可能是底层共享的 buffer,直接改可能崩掉。正确的做法是返回一个新的字符串。transform钩子的可选meta参数更规范,但要求使用新的签名顺序。说白了就是:你的函数签名要写transform(code, id, options),不要再去拿第二个参数当作选项对象来用。这是很细节的破坏性变更,但网上已经有不少插件作者中招了。
4.3 对新写插件的人:API 简化在哪里
如果你是新手,想用 Vite 写第一个插件,8.0 其实是更友好的。因为官方推荐的新写法把“虚拟模块”创建和“自定义解析路径”合并成一个更直观的assets配置概念。以前想给项目加一个虚拟模块virtual:foo,你得同时写resolveId和load两个钩子。现在可以直接在插件里定义一个module对象,声明它叫virtual:foo,然后实现generate方法就行了。
另外,在新增的renderChunk阶段,Rolldown 提供了更丰富的 chunk 元信息,包括每个 chunk 的“物理大小”“gzip 估算大小”“引用关系图”。做性能分析类插件的人以前要自己拼这些数据,现在直接读钩子参数就行。我自己写了一个统计构建产物体积的插件,利用这个新接口,代码量几乎少了一半。
5. 从旧版本到 8.0 的迁移实操记录
5.1 我遇到的第一个坑:依赖预构建配置项失效
升级完先把package.json里的vite版本改成^8.0.0,然后跑一下npm install,再启动开发服务器。我用的是 Vite 7 项目,配置里有这样一段:
optimizeDeps: { include: ['axios', 'react', 'react-dom'], exclude: ['monaco-editor'], }结果 8.0 上来就把它忽略了。后面读文档才知道,新版把optimizeDeps.include和exclude的语义做了修改:因为 Rolldown 的处理速度太快,默认行为已经改成“启动时全量扫描依赖”,不再像以前那样需要你用include去手动往预构建列表里塞一些扫描不到的东西。如果你确实需要强制排除某些依赖,现在要用新的顶层配置键defineConfig.deps.exclude而不是写在optimizeDeps下面。
这个升级会带来的实际影响是:所有依赖扫描到的包都会在启动时被处理,如果你的项目里有几个体积巨大的依赖,并且它们相互之间没有依赖关系,那么启动速度会比以前更依赖磁盘 IO。但总体体验仍比旧版快,只是你不再需要用“人工 include”去骗 Vite 扫描了。
5.2 不同版本迁移的差异速查表
我整理了一份从几个常见版本升到 8.0 时需要关注的核对表,方便你对照着自己排查。
| 原版本 | 最可能的破坏点 | 建议处理方式 |
|---|---|---|
| Vite 5 | 插件钩子签名与 transform 代码形式变化 | 逐一更新有transform或enforce的插件,先跑一次 build 看报错 |
| Vite 6 | optimizeDeps配置被废弃或半废弃 | 改用顶层deps配置,移除过时字段 |
| Vite 7 | 依赖预构建目录变了,SSR target 默认值不同 | 清理node_modules/.vite旧缓存,显式指定build.target |
| 老自定义插件 | resolveId返回格式可能不再兼容 | 用新的module对象 API 重写,或确认返回值是合法的解析结果 |
这里明确说一下,Vite 7 本身已经踩在 Rolldown 迁移的路上了,8.0 更多是收尾和稳定,所以从 7 升级最轻松;如果你是从 5 或 6 直接跳,那改动量会稍微大一点,尤其是插件生态适配方面。
5.3 构建目标与产物格式的变化
8.0 对build.target的默认值也做了调整,过去默认modules对应的目标大约等价于“支持原生 ESM 的现代浏览器”,现在它进一步提高了基线:像 Chrome 111 以上、Safari 16 以上这类版本才会被当作现代浏览器,放弃了对更老浏览器的自动语法转换。如果你还在维护需要兼容老旧浏览器的项目,升级后必须在build.target里显式指定一个更低的版本,例如:
export default { build: { target: 'chrome87', }, }同时,模块格式的默认值也更倾向于输出标准的 ESM chunk,而不是以前那种经过较多兼容处理的形态。如果你依赖的是某些服务端处理过的dist产物,比如直接把整个文件夹丢到特定 CDN 上运行,可能需要重新检查资源引用路径。反正我的建议是:把dist产物跑一遍真实的部署流程再做兼容测试,不要只在本地看页面挂了没挂。
6. 性能实测与一线观察:变快有多快
6.1 冷启动与热更新的大致量级
我不想报一个精确到第三位的“理论成绩”,因为环境不同、项目复杂度不同,性能数字很难横向比较。但从我自己以及身边几个项目的情况来看,还是能给出一个非常宽泛但可信的范围:
- 冷启动时间普遍下降 50% 到 70%,千级模块项目从 5 秒左右降到 2 秒左右。
- 批量文件保存触发的 HMR 更新,从“明显感觉慢”降到“没什么感觉”。这只是把
pages目录下十几个文件同时改动,旧版要重新计算依赖链,新版基本变化在几百毫秒内。 - 依赖预构建在首次启动时几乎不再成为瓶颈,除非你装了一个特别大的、内容特别多的包。
对一个已经做了很多轮性能优化的老版本 Vite 项目来说,这个再次下降的量级其实很惊人。说明“换引擎”这种改动,即使上层配置看起来没变,实际性能收益也是稳定的。
6.2 Monorepo 场景下的实际体验
Monorepo 是这次升级里受益最大的场景。传统的 Vite 在 monorepo 里跑,最大的痛点是依赖预构建要同时处理工作区内部链接的包和外部的 npm 包,链接关系一多,扫描就非常慢。8.0 的 Rust 引擎对工作区里的软链接解析做了更高效的处理,同时在启动时就开始并行扫描多个包,不会像旧版那样挨个处理。
我拿一个 lerna+yarn workspaces 的项目做过测试,项目里有 20 多个子包,依赖树总节点接近 8000 个。旧版第一次冷启动是 14 秒左右,其中有 8 秒多花在依赖解析和预构建上。升级到 8.0 后,同样的仓库在同样环境下冷启动大概 4.2 秒,后续重启开发服务器时因为有了持久化缓存,可以压到 2 秒内。这个提升对 monorepo 里每天要重启若干次服务器的人来说,属于非常实用的节省。
6.3 构建产物体积有没有变化
一个大版本的性能提升之外,大家也很关心产物体积。8.0 在产物体积方面没有做激进的多级分包划分,默认的代码分割策略跟 7 代差别很小。但因为它底层生成的代码去除了更多冗余的 interop 包装,实际产物体积通常会有 2% 到 5% 的下降。不要指望这种体积优化能替代你手动做代码分割,但它确实是白赚的。
我试过一个比较纯粹的 React + TS 项目,构建后的 gzip 体积从 235KB 降到 228KB,幅度大概 3%。改动不大,但对一个已经优化很久的项目来说,能有这个幅度已经说明底层生成器确实比之前更擅长输出简洁的代码。
7. 常见问题与排查思路实录
7.1 升级后终端报错“Cannot find module”
这种报错大多不是 8.0 本身的问题,而是升级后依赖树里同时存在多个 Vite 版本导致的。Node 在解析vite模块时,如果你没有把vite从devDependencies统一到同一个版本,子依赖里某个插件可能锁定了它自己依赖的另一个 Vite 副本。解决办法很简单:在根目录或package.json里做一次overrides,把所有vite版本改成8.0.x,然后删掉node_modules重新安装一遍。
{ "overrides": { "vite": "^8.0.0" } }这招能解决 90% 的Cannot find module问题。剩下 10% 则可能是某些旧插件访问了 Vite 内部的不稳定目录,这种只能把插件版本升到支持 8.0 的版本。
7.2 插件兼容性排查步骤
如果你升级之后发现某个插件不触发或者行为异常,先按这个顺序排查:
- 确认插件版本是否发布了支持 8.0 的更新版本,更新到最后。
- 看插件的源码里是否引用了
vite包的内部模块,比如vite/dist/node/…这样的深层路径,如果有,基本就废了,需要等插件作者重构。 - 用最小示例逐步注释掉插件,确认是它的哪个钩子引发问题。
- 在
transform钩子内部用console.log输出拿到的 code 和 id,对比到底和旧版差在哪。
这个排查方法我在 Vite 6 升 7 的时候就用过,到 8.0 依然有效。关键是别拿一个几十个插件的大项目直接试错,先做一个最小 reproductions,事半功倍。
7.3 产物体积或内容变化的应对
如果你升级后构建出来的 dist 内容和旧版差异很大,先别急着回退。很多时候是因为默认 target 变了,导致代码里的工具函数展开方式不一样。比如新版不再加载某些 polyfill,因为目标浏览器已经原生支持了。这种变化是预期的,你需要做的是部署后跑一轮回归测试。
如果差异实在太大,可以先手动在build.target里指定旧版浏览器版本,再对比一次。如果指定到chrome87之后产物体积和旧版接近,那基本可以判断就是 target 基线调整导致的。
8. 最后分享一个小技巧
我自己在后面几天继续用 8.0 时,发现一个新的好处:它在处理构建日志时会把最耗时的几个模块直接排序列出来,并且标注“这个模块为什么不能缓存”。你只要在终端里开满DEBUG=vite:*模式跑一次构建,就能看到一张模块耗时分析表。这张表对定位项目里的性能瓶颈非常有用,比以前靠猜或者靠 Chrome Performance 去翻网络请求直观多了。
希望你升级顺利。如果遇到怪问题,建议先跑一个vite --debug看看底层到底在哪一步停住,通常比查论坛快很多。