Vite 8.1 打包开发模式——10000 组件冷启动 15 倍,"No Bundle Dev" 七年后被自己终结

2026 年 6 月末,Vite 8.1 正式发布。这个版本没有破坏性变更,但发布公告标题里那句话很反常:

Bundled Dev Mode------开发时打包,大型项目冷启动最高 15 倍提速。

Vite 从 2020 年立项到 8.0 一路封神,靠的就是一个卖点------"开发时不打包"(No Bundle Dev)。它用原生 ES Module 让浏览器按需请求模块,把 webpack 那种"全量打包才能启动"的模式钉在了历史的耻辱柱上。

现在 Vite 自己站出来说:开发时也可以打包,某些项目应该打包。

不是回头路,是承认------七年前对的假设,2026 年的项目规模已经撑不住了。

这篇文章拆三层:为什么"不打包"这条路开始走不通、Bundled Dev Mode 到底做了什么、什么样的项目现在就该开。文末给了一份可直接用的 Vue + Vite 8.1 迁移清单和 bench 脚本。


一、Vite 最引以为傲的设计,为什么开始撑不住了

先复盘 Vite 的原始设计。

2020 年的前端世界,webpack 是霸主。它的工作模式是:npm run dev 一按,先把项目的所有源码 加上所有依赖塞进依赖图,一起打成一个(或者几个)bundle,然后开发服务器再启动。项目越大,启动越慢------十几个 Vue 页面 30 秒,几十个页面等 1 分钟以上。

Vite 干了一件在当时看起来"反常识"的事:dev server 启动时不打包

它的逻辑很直接:

  • 浏览器已经原生支持 <script type="module">import
  • 那为什么还要在服务端把模块拼成 bundle?
  • 直接让浏览器按需请求模块,需要哪个就发一个请求
  • Vite 只负责把 TS / JSX / Vue SFC 现场转成浏览器能读的 JS,然后回给浏览器

这个模式的收益是恐怖的------冷启动从 30 秒变成 300 毫秒,快了 100 倍。项目多大都无所谓,因为首屏只请求首屏用到的模块,而不是把整个项目打包出来。

于是 Vite 用五年时间干掉了 webpack。到 2026 年,Vite 的周下载量已经稳定在 1.4 亿以上,比 Vite 7 累计下载量还多。

问题从大型项目开始冒出来

一个 Vue 企业级应用,几百个页面、几千个组件、上万个模块。你以为 Vite 会秒启动?

现实是:

  • 依赖预构建(把 node_modules 里的 CJS 包转成 ESM)第一次要跑几十秒
  • 首屏一打开,浏览器要向 dev server 发几百个 module 请求
  • 每个请求都要走一遍插件链:resolve、load、transform、HMR 注册
  • 全量刷新(Full Reload)时,浏览器要重新发几百个请求,几秒钟没画面
  • 你的路由跳到新页面,又是几十上百个模块要拉

Vite 官方也承认了这一点,8.1 发布公告里写得很直白:

真正的问题已经不是构建耗时,而是模块数量爆炸导致开发服务器请求成本失控。

这个问题在小项目里根本不存在,但一旦项目突破 5000 模块(一个中等 SaaS 后台就到了),"每个模块一次请求"的物理开销开始压过"不打包"的收益。

打个比方:Vite 原来的模式像便利店,你要什么拿什么,来去自由。但当你要买 1000 件商品时,一件一件排队结账反而比一次性推购物车结账慢得多。

二、Bundled Dev Mode:不是回到 webpack,是"用 Rolldown 提前打好,用 HMR 精准替换"

Vite 8.1 的解法叫 Bundled Dev Mode(打包开发模式)。名字听起来像回到 webpack,但实现思路完全不同。

传统 Vite dev(保留):

css 复制代码
启动项目 → 只做依赖预构建 → 开发服务器就绪
浏览器请求 → 逐个模块 transform → 发回浏览器(几百次请求)

Bundled Dev Mode(新增,默认关):

复制代码
启动项目 → Rolldown 一次性打包整个应用 → 开发服务器就绪
浏览器请求 → 直接拿打好的 chunk → 一次或几次请求搞定

关键不同有三处:

① 打包器换成了 Rolldown(Rust 写的) Vite 8 已经把生产构建从 esbuild + Rollup 换成了 Rolldown 单打包器。8.1 让开发模式也复用这条 Rust 链路,等于是"生产构建器 + 保留 HMR 的能力"。Rust 的原生性能保证了打包耗时是可接受的------它不是 webpack 那种 JS 慢慢跑的打包器。

② HMR 依旧走精准热替换 这是最容易被误解的地方。开了 Bundled Dev 之后,你依然能得到秒级 HMR。改一个 Vue 组件,只有那个组件被替换,页面状态保留。Vite 团队为此在 Rolldown 里做了 chunk 级的 HMR 边界追踪------打包只发生在启动那一下,之后的热更新走的还是精准替换。

用他们自己的话:

Bundled Dev Mode 的核心是"预打包 + 精准 HMR",不是"每次改动全量重打"。

③ 首屏请求数从几百下降到个位数 这是 Linear 团队实测最夸张的一条:Linear 首屏请求数减少 90%。原来几百次 module fetch,现在几个 chunk 打包好一次拉完。移动端和弱网环境下体感差异极大。

开启方式

方式一,写进 vite.config.ts

ts 复制代码
// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  experimental: {
    bundledDev: true,
  },
})

方式二,命令行临时开:

bash 复制代码
vite --experimental-bundle

两种都行,前者持久化写在配置里,后者适合 A/B 对比。注意 experimental 前缀------这仍然是实验特性,8.1 里不是默认打开。


三、一手实测数据:三组硬指标

Vite 8.1 发布时官方给了两组基准数据,加上 Linear 团队的生产项目实测博客,我把它们放在一张表里:

场景 传统 Vite Dev Bundled Dev Mode 提升倍数
10000 React 组件冷启动 基准 1/15 15x
10000 React 组件整页重载(Full Reload) 基准 1/10 10x
Linear 生产项目冷启动 基准 1/3 3x
Linear 页面全量重载 基准 -40% 1.67x
Linear 首屏模块请求数 基准 1/10 10x(−90%)
HMR 单组件热更新 ≈50ms ≈50ms 不变

据《Vite 8.1 正式发布!启动提速 15 倍,加载提速 10 倍》(juejin.cn/post/765605...),这些数字来自官方基准仓库和 Linear 团队的生产环境实测博客。

有三个细节值得单独拎出来:

① "10000 组件"是官方压测基准,不代表你的项目

15 倍这个数字是 Vite 团队构造的极端场景------10000 个 React 组件的合成项目,模块数量刻意堆到极限。你自己的项目大概率没这么夸张,收益不会是 15 倍。

真实项目的参考值是 Linear:中等规模的 SaaS 后台,冷启动 3 倍,全量重载 1.67 倍,请求数减少 90%。这才是可参考的量级。

② HMR 单组件热更新几乎不变

这一条特别重要。很多人担心"打包了 HMR 是不是就变慢了"------不会。Vite 8.1 保留了 Vite 原有的精准 HMR 机制,改一个组件就替换一个 chunk 的模块。日常写代码时的体感和以前一样。

Bundled Dev 换掉的只有冷启动 + 全量刷新这两条"慢路径"。热开发流程不受影响。

③ 请求数减少的连锁收益

首屏请求数 −90% 不只是"快 10 倍"这么简单。它还带来:

  • 弱网 / 移动 dev 环境体验大幅改善
  • Chrome DevTools 的 Network 面板可读性提升(原来几百条请求,现在几条)
  • 反向代理(比如你在 Electron 里用自定义 protocol 加载 dev server)的调试成本下降
  • CI/CD 场景下 e2e 测试的启动更稳定

四、内部原理:为什么 Rolldown 能"打包一次不影响 HMR"

这一节偏原理,赶时间可以跳到第五节看决策。

Vite 8.1 的实现分四步:

第一步:启动时全量 Rolldown 打包

npm run dev 时,Rolldown 会走一遍完整的模块解析 + 打包流程,但产出的不是 dist/ 里的最终 chunk,而是"dev 优化过"的 chunk------保留了模块 ID、source map、HMR 客户端注入点。这个过程用 Rust 完成,即便是 10000 模块的项目,也就几秒钟。

第二步:dev server 直接分发这些 chunk

浏览器打开页面,dev server 分发的不再是"每个模块一次请求",而是几个大 chunk。首屏可能只有 3-5 个 chunk(app、vendor、runtime 之类)加上 CSS。请求数从几百降到个位数,这是 −90% 数据的物理来源。

第三步:文件变化时精准热替换

你改了一个 Vue 组件(比如 Foo.vue):

  1. Vite dev server 观察到文件变化
  2. Rolldown 只重新处理 Foo.vue 单个模块(不是整个 chunk)
  3. 生成一个 patch,通过 HMR 通道推送到浏览器
  4. 浏览器执行 patch,替换那个模块的导出,触发 Vue 的组件级重挂载

这一步是关键------它不会重新打包整个 chunk,只推送模块级 diff。所以 HMR 延迟依然是几十毫秒。

第四步:热更新累积到一定量再重打

如果你连续改了几十个文件(比如 rebase 之后拉了一大堆变更),HMR 会累积 patch。当累积超过阈值时,Vite 会在后台重新触发一次 Rolldown 增量打包,把状态"归一"。这个过程对开发者是透明的,你不会看到"全量刷新"。

为什么这条路 esbuild 走不通

有人问:既然要打包,为什么不继续用 esbuild?8.0 之前的 Vite 用 esbuild 做依赖预构建也很快啊。

答案在 Vite 团队的解释里:esbuild 的模块图不支持精准 HMR。它是"打好扔出去"的一次性工具,不维护模块间的热更新边界。你要把它改造成"能 HMR 的 dev bundler"成本极高。

Rolldown 从设计之初就把 dev 和 prod 视为同一条 pipeline 的两种输出。开发模式在 Rust 层维护模块图和 HMR 边界,改一个文件时能精准定位到"哪个 chunk 的哪个模块要被 patch"。这是 esbuild 架构上做不到的。

据《Vite 8 + Rolldown Migration Guide》(nexgismo.com),Rolldown 是"purpose-built for Vite"(为 Vite 定制)。8.1 的 Bundled Dev Mode 是这个定制关系兑现的第一个大特性------dev 和 prod 用同一条 Rust 打包链


五、什么时候该开、什么时候先别开

Vite 官方明确说 Bundled Dev 是"新增选项,不是替换"。小项目继续 no-bundle,大项目按需切换。给一份决策表:

项目类型 建议 原因
组件库 / demo / 教程项目(<100 模块) 别开 no-bundle 秒启动已经足够,打包反而多花几百毫秒
中小型 Web 应用(<1000 模块) 可以试,但收益有限 冷启动可能从 2s 到 1s,体感提升不大
Vue 3 中后台 SaaS(1000-5000 模块) 建议开 冷启动 5-10s 是常态,能砍到 1-3s
大型 Monorepo / 企业级应用(>5000 模块) 必须开 no-bundle 已经痛到不可用,收益最大
Electron 主进程 + 渲染进程双端项目 建议开 dev 时 renderer 端首屏请求数是主要 bottleneck
AI 平台 / 低代码平台(模块动态生成) 谨慎试 动态 import 多、模块生成路径复杂,可能踩坑
生产环境 / preview 分支 别开 experimental 阶段,生产别用

三条"暂时不能开"的硬约束

① 用了不兼容的第三方插件

官方明说:Bundled Dev Mode 只保证兼容 Vite 官方基础插件(@vitejs/plugin-vue@vitejs/plugin-react@vitejs/plugin-legacy 等)。第三方插件如果依赖了旧 Vite 的模块图 API(load / transform 的执行顺序假设),可能会挂。

先在业务分支上开一天看看,别直接推 main。

② 用了 experimental.renderBuiltUrl

8.1 新增的 Chunk Import Map 和 renderBuiltUrl 互斥。如果你项目里用了 renderBuiltUrl 做资源 URL 重写(很多 CDN 部署场景会用),Bundled Dev 里的 Chunk Import Map 相关能力会与之冲突。

③ 项目里有大量 dynamic import 且路径是动态字符串

Rolldown 静态分析 dynamic import 时对"完全动态的路径字符串"支持不如运行时按需加载。比如:

ts 复制代码
// 静态可分析 ------ 没问题
const mod = await import('./views/Home.vue')

// 半动态 ------ Rolldown 用 glob 也能处理
const mod = await import(`./views/${page}.vue`)

// 完全动态 ------ Bundled Dev 可能定位不到
const mod = await import(rawPath)

第三种情况在插件系统、微前端、低代码场景常见,升级前先 grep 一下 import( 的用法。


六、Vue 项目实操:三步试水 + bench 脚本

对 Vue 生态来说,Vite 8.1 的适配没有任何摩擦。@vitejs/plugin-vue 官方已经跟 Rolldown 完全对齐。

第一步:确认 Node 版本

Vite 8 要求 Node.js 20.19+ 或 22.12+。本地、CI、Dockerfile 都要对齐。Electron 项目额外要注意 renderer 端的 Node 版本(Electron 内嵌 Node):

bash 复制代码
node -v      # 至少 v20.19 或 v22.12

第二步:安装 Vite 8.1

bash 复制代码
pnpm add vite@latest

如果你之前是 Vite 7,中间会自动经过 Rolldown 迁移。8.0 已经跑通的项目升 8.1 是无缝的。

第三步:配置 Bundled Dev(先建业务分支再改)

ts 复制代码
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  experimental: {
    bundledDev: true,          // 主开关
  },
  // Chunk Import Map 优化缓存(可选,与 renderBuiltUrl 互斥)
  // experimental: {
  //   bundledDev: true,
  //   chunkImportMap: true,
  // },
})

bench 脚本:跑 5 次取平均值再决策

别凭感觉。第一次跑可能有 node_modules 缓存 warm-up,第二次可能后台在跑别的任务。放一个脚本进项目:

js 复制代码
// scripts/bench-dev-startup.mjs
import { spawn } from 'node:child_process'
import { performance } from 'node:perf_hooks'

const RUNS = Number(process.env.RUNS ?? 5)
const READY_PATTERN = /ready in \d+/i
const results = []

async function runOnce(index) {
  return new Promise((resolve) => {
    const start = performance.now()
    const proc = spawn('pnpm', ['dev'], { stdio: ['ignore', 'pipe', 'pipe'] })
    let done = false

    proc.stdout.on('data', (chunk) => {
      const line = chunk.toString()
      process.stdout.write(line)
      if (!done && READY_PATTERN.test(line)) {
        done = true
        const cost = (performance.now() - start) / 1000
        results.push(cost)
        console.log(`\n[run ${index}] ready in ${cost.toFixed(2)}s`)
        proc.kill('SIGTERM')
        resolve()
      }
    })

    proc.on('exit', () => {
      if (!done) {
        console.log(`[run ${index}] failed to detect ready`)
        resolve()
      }
    })
  })
}

for (let i = 1; i <= RUNS; i += 1) {
  await runOnce(i)
  await new Promise((r) => setTimeout(r, 2000)) // 冷却 2s
}

const avg = results.reduce((s, v) => s + v, 0) / results.length
const min = Math.min(...results)
const max = Math.max(...results)
console.log(`\navg=${avg.toFixed(2)}s min=${min.toFixed(2)}s max=${max.toFixed(2)}s`)

跑两组对比:

bash 复制代码
# 组一:传统模式(把 vite.config 里 bundledDev 改成 false)
RUNS=5 node scripts/bench-dev-startup.mjs

# 组二:Bundled Dev
RUNS=5 node scripts/bench-dev-startup.mjs

对比 avg 就知道你的项目值不值得开。经验值:

  • avg 提升 <20%:收益不明显,别切
  • avg 提升 20%-50%:看团队痛点程度,如果每天都吐槽启动慢就切
  • avg 提升 >50%:直接切,日均省几分钟就是几百块钱工资

Electron 场景的一个坑

Electron 项目里,renderer 通常跑在 dev server 上。开了 Bundled Dev 后,第一次启动 renderer 会比原来慢一点(因为要打包),但一旦 dev server 起来了,Electron 主进程 reload renderer 会快得多------请求数少了,dev server 分发速度快得多。

这个 trade-off 在 Electron 场景通常是划算的,因为你在 Electron 里一天 reload 几十次 renderer,冷启动只有一次。


七、Vite 8.1 顺带的四个改进(都能马上用)

Bundled Dev 是本次头条,但 8.1 还塞了四个小改进,几乎每个都有实用价值。

① Chunk Import Map:解决"改一行工具函数,全站缓存失效"

老问题:你改了一个 utils 函数,Rolldown 会重算这个模块的 chunk hash。因为其他 chunk 依赖了这个 hash,它们的 hash 也跟着变------用户浏览器缓存的旧文件全部失效,重新拉一遍。

Chunk Import Map 用浏览器原生的 Import Maps 特性把这个链路解耦:

  • 每个 chunk 用逻辑 ID (比如 utils.js)而不是 hash 路径引用其他 chunk
  • 具体 hash 到路径的映射写在 HTML 里的 <script type="importmap">
  • 改 utils 只影响 utils 那一个 chunk 的 hash 和 import map,其他 chunk 完全不动

结果:大项目首屏加载时的浏览器缓存命中率显著提升

ts 复制代码
// vite.config.ts
export default defineConfig({
  experimental: {
    chunkImportMap: true,  // 开启
  },
})

⚠️ 注意 :这个特性和 experimental.renderBuiltUrl 互斥,只能二选一。

② Wasm ESM:不装插件直接 import .wasm

以前你要在项目里用 WebAssembly:装 vite-plugin-wasm,配 loader,写一堆 magic import 语法。

8.1 直接内置了 Wasm ESM 标准:

ts 复制代码
import { add } from './add.wasm'

console.log(add(1, 2)) // 3

对于用了 Rolldown / Oxc / SWC / Biome 这些 Rust 工具链的项目,未来它们的 WASM 版本可以直接在浏览器跑。

你的 Electron 项目如果有性能敏感的计算逻辑(比如图像处理、音频分析),可以考虑用 Rust 写完编译成 wasm,Vite 里直接 import,比 native binding 简单得多。

③ Lightning CSS 补齐短板:可以用外部 CSS 和插件依赖了

Vite 8 已经默认用 Lightning CSS 做 CSS 压缩,但当时有些边缘场景(外部 CSS 引入、插件自定义依赖)不支持。8.1 补齐了。

现在可以整体把 CSS 转换从 PostCSS 切到 Lightning CSS,编译和压缩都更快:

ts 复制代码
export default defineConfig({
  css: {
    transformer: 'lightningcss',
  },
})

官方明说:未来大版本会把 Lightning CSS 设成默认。现在切过去,等于提前对齐未来。

import.meta.glob 支持 caseSensitive 选项

老 bug:Windows 文件系统不区分大小写,Mac 区分。你在 Windows 上写 import.meta.glob('./views/**/Page.vue'),两个都匹配;一 push 到 CI(Linux),只有 Page.vue 匹配,page.vue 就漏了。

8.1 加了显式选项:

ts 复制代码
const modules = import.meta.glob('./dir/module*.js', {
  caseSensitive: false,  // 显式跨平台大小写不敏感
})

场景边缘,但踩过的人都知道疼。加上这一行防线,跨平台协作稳一半。


八、一次教科书式的自我否定

写到这里可以退一步看这次升级的叙事。

Vite 从 2020 年起,靠"No Bundle Dev"这一个核心创新终结了 webpack。这套设计的假设是:

浏览器原生 ESM + 按需加载 > 预打包 + 服务端分发

七年过去,这个假设开始出现裂缝------在小到中等项目上依然成立,在大型项目上被模块数量的物理开销压垮

Vite 8.1 做的事,本质是承认这一点。它不是抛弃 No Bundle Dev(小项目继续用),而是新增一条"打包路径"作为大项目的兜底。你会发现这个设计和当年 Vite 挑战 webpack 时的对比表几乎反过来:

2020 年 Vite vs webpack 2026 年 Vite 8.1 vs Vite 7
冷启动策略 不打包,按需加载胜出 打包 + 精准 HMR 胜出
请求数 少 vs 多 少 vs 多
首屏体验 秒开胜出 秒开胜出
大项目痛点 webpack 30s+ Vite 无打包也 10s+

指标没变,赢家换了。因为项目规模变了,物理约束的分界线跟着移。

我觉得这才是这次升级真正值得写的地方------技术选型没有永远正确的答案。当年做 No Bundle Dev 是对的,2026 年做 Bundled Dev 也是对的。工具的价值不在于"我这套设计多么优雅",而在于"我能持续解决用户当下的问题"。

Vite 团队敢在自己最引以为傲的设计上开一个"回头路"的实验特性,还是需要点勇气的。这比很多"我们的架构十年不变"的产品负责得多。


九、升级 checklist(可复用)

发一份可以直接抄的 checklist 给团队:

ini 复制代码
[ ] Node.js 版本对齐 20.19+ 或 22.12+(本地/CI/Docker/Electron)
[ ] pnpm add vite@latest(升到 8.1)
[ ] 检查所有第三方 Vite 插件的 Rolldown 兼容声明
[ ] 在业务分支开启 experimental.bundledDev
[ ] 运行 bench-dev-startup.mjs 跑 5 次冷启动对比
[ ] 手动 reload 5-10 次,测 HMR 是否正常
[ ] 检查是否用了 experimental.renderBuiltUrl(与 Chunk Import Map 互斥)
[ ] grep 全项目 import( 用法,排查完全动态路径
[ ] 生产环境保持 build 走 Vite 8 原生(不受 Bundled Dev 影响)
[ ] 团队内部对齐使用规范,避免有人开有人不开导致 bench 数据混乱

以上任何一步失败就打住,别硬升。8.1 是好特性,但 experimental 不适合直接推生产依赖的开发工作流。


参考资料


本内容由AI辅助生成

相关推荐
YHL1 小时前
🤖 Agent 智能体开发实战 · 第一课:Tool Use —— 让大模型自动干活
前端·javascript·人工智能
Monki1 小时前
AI 规范式 UI 设计,一键批量出页,高效落地不翻车
前端
山河木马1 小时前
渲染管线-计算得到gl_FragColor(片元着色器)之后续GPU流程
前端·webgl·计算机图形学
不想说话的麋鹿1 小时前
「项目实战」我用 Codex + GPT-5.5 + Trellis "氛围编程",独立做了一个情侣厨房小程序
前端·agent·全栈
接着奏乐接着舞。2 小时前
【2026年7月最新】69道RAG面试题
前端·人工智能·后端·aigc·embedding·rag
以和为贵2 小时前
前端也能搞懂 Agent:从 Function Calling 到自主编排
前端·人工智能·架构
HUMHSX2 小时前
Vue 组件通信核心知识点梳理
javascript·vue.js·ecmascript
风止何安啊3 小时前
🚦 前端并发请求 “交通管制”:别让你的接口堵成早高峰
前端·javascript·面试
J船长3 小时前
扫盲烂笔头:A 记录、CNAME 的作用, Nginx的用途,为啥叫反向代理
前端