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辅助生成

相关推荐
SameX2 小时前
HarmonyOS ArkUI Dialog 复用 用 @CustomDialog + enum type 做多类型统一管理的做法
前端
AiMan2 小时前
豆包爱学还是不可以导出左侧所有对话,只能单容导出,方法不对吗
前端
无懈可击2 小时前
Vue 可视化AI表单设计器 FcDesigner v3.6 版本发布,支持导出 SQL功能!
vue.js·开源·ai编程
AiMan2 小时前
Grok生成的内容能否下载吗?
前端
徐小夕3 小时前
JitArchives,一款基于JitWord协同AI文档打造的企业级电子档案系统
前端·vue.js·github
陈随易3 小时前
Node.js 2026上半年都有哪些性能提升?
前端·后端·程序员
墨林陌3 小时前
AI 热点日报(2026-11-09):苹果代理式Siri曝光,Anthropic推进IPO,编码智能体实测翻车
前端
vipxieliang3 小时前
JavaScript 迭代器与生成器:控制异步与惰性求值
前端·javascript
Hilaku3 小时前
Event Loop 这篇你看完就不用再看别的了!
前端·javascript·程序员
恋猫de小郭4 小时前
Flutter 下 AOT 一个你怎么都想不出来的 Bug ,JIT 正常的计算,在 AOT 居然算错了
android·前端·flutter