Skip to content

同样是启动开发服务器,Webpack 项目越大越慢,Vite 却永远是秒开。差异不是「优化得好」,而是思路根本不同:Webpack 在 dev 时也要先打包,Vite 干脆不打包。这篇把 Vite 快的三个原因讲透:原生 ESM、依赖预构建、HMR,以及为什么生产构建反而换回 Rollup——dev 和 build 是两套引擎

一、总览:Vite 的三层加速

Vite 快的秘密可以拆成三层,每层解决一个问题:

手段解决什么
第一层dev 不打包,浏览器原生 ESM 加载启动时不用等全量打包
第二层依赖预构建(esbuild,Go 写的)源文件不用管,依赖先转 ESM 并合并
第三层HMR:按模块热更新改一行代码只重载那一个模块

Vite dev 与 build 双引擎架构

二、第一层:dev 不打包,浏览器自己拉模块

Webpack 的 dev 模式也要先打包:分析全部模块依赖、编译、合并成 bundle,浏览器拿到 bundle 才能跑。项目 1000 个模块就要处理 1000 个,启动自然慢。

Vite 的做法:dev 时源码原样通过 <script type="module"> 发给浏览器,浏览器原生支持 ES Module,自己按 import 语句一条条拉取。Vite 只做一个轻量转换(把 .vue.ts 编译成浏览器能认的 JS),不做打包。

对比之下:

Webpack dev:启动 → 打包全部 1000 个模块 → 浏览器加载 bundle
Vite   dev:启动(秒)→ 浏览器按需加载第一个模块 → 用到哪个再拉哪个

所以 Vite 的启动时间跟项目规模基本无关——浏览器还没开始拉模块呢,服务器已经就绪了。用到的具体机制见 Vite 快速入门 的「dev 时根本不用打包」。

三、第二层:依赖预构建,esbuild 的活

源文件不打包,但第三方依赖要预构建——Vite 启动时先把 node_modules 里的依赖处理一遍,产物缓存在 node_modules/.vite/deps/。为什么要预构建?两个原因:

原因 1:CommonJS → ESM 转换。很多老包(lodashreact-dom 的老版本)用的是 CommonJS(module.exports),浏览器不认识,必须转成 ESM 才能直接 import。

原因 2:合并请求数。假设你 import { cloneDeep } from 'lodash-es',lodash-es 内部有 600 多个模块文件,浏览器如果逐个拉,一次页面加载就要发 600 个请求。预构建把这 600 个合并成一个文件,浏览器只发一个请求。

实跑证据(Vite 8):dev 起一个 import { cloneDeep } from 'lodash-es' 的项目,浏览器收到的源码里 import 已被重写成 /node_modules/.vite/deps/lodash-es.js——预构建合并产物;同时 build 时终端打印 645 modules transformed——Rollup 逐个处理这些模块做 tree-shaking,只用到两个函数,最终产物仅 15 kB。

预构建工具用 esbuild——用 Go 写的,比 JS 写的打包器快 10~100 倍(官方说法 10-100x)。这也是 Vite 启动快的第二层来源:依赖虽然要处理,但处理得极快。

四、第三层:HMR,改哪刷哪

HMR(Hot Module Replacement,模块热替换):修改一个模块,只重新加载这一个模块,页面状态(输入框内容、滚动位置、已打开弹窗)全部保留。

Vite 维护一张模块依赖图。你改了 table.js,Vite 通过 WebSocket 通知浏览器「table.js 变了」,浏览器只重新请求 table.js、替换它,其他模块原封不动:

改 table.js → 通知浏览器 → 只重新拉取 table.js → 页面其余部分不动

对比 Webpack:改动后它要重新编译包含该模块的整个 chunk,chunk 越大刷新越慢。Vite 是按模块粒度的,所以项目越大,两者体验差距越明显。

模块自己也决定「热不热」:组件文件默认支持 HMR(Vue/React 插件接入);普通 JS 模块如果没声明 import.meta.hot.accept(),改了它会触发整页刷新兜底——所以写库代码时经常看到保存后页面闪一下,那就是走了兜底。

五、为什么 build 反而用 Rollup?

这是 Vite 最容易被问到的设计:dev 用 esbuild(快),build 却换回 Rollup(JS 写的,慢一些),为什么不一路 esbuild 到底?

官方理由很实在:

能力esbuildRollup
转译/压缩速度极快(Go)较慢(JS)
tree-shaking有,较激进成熟、精准
代码分割(code splitting)支持有限成熟(动态 import 拆 chunk)
插件生态较新十几年积累
CSS 处理简单完整(CSS 提取、代码分割)

构建产物的目标是「稳」而不是「快」:tree-shaking 摇错一个变量就是线上 bug,代码分割做不好就是首屏加载一堆用不到的代码。Rollup 在这两件事上积累了十几年,Vite 生产构建选择「成熟优先」;esbuild 也没闲着,被用作默认的压缩器(minify),快的部分让它干。

一句话记忆:dev 拼速度(esbuild + 原生 ESM),build 拼质量(Rollup),各自干擅长的

总结

问题答案
为什么 dev 秒启动不打包,浏览器原生 ESM 按需拉模块
为什么依赖要预构建CJS 转 ESM + 600 个模块合并成 1 个,减少请求
预构建用什么esbuild,Go 写的,比 JS 快 10-100 倍
为什么 HMR 快按模块热更新,改哪刷哪,状态不丢
为什么 build 用 Rolluptree-shaking / 代码分割成熟,构建求稳不求快

链式导航:Vite 怎么用(三条命令、别名、代理、环境变量),看 Vite 快速入门;Vite 编译的 .vue 文件本质是模板编译,配合 Vue 响应式原理详解 理解组件更新;构建产物 hash 缓存策略可以对照 Vue 快速入门 的静态资源部分。