要回答清楚,我们需要把 Vite 的“快”拆分成两半来看,因为在开发环境(npm run dev)和生产环境(npm run build)**,它快的原理是完全不一样的。
这里有一个巨大的误区:Vite 开发时之所以快,并不是因为用了 Rollup,而是因为根本没打包! 而生产环境用 Rollup,主要不是为了构建速度快,而是为了代码产物体积小(跑得快) 。
第一部分:开发环境(Dev)为什么秒开?—— “根本不打包” + Esbuild
以前用 Webpack (Vue CLI) 启动项目,随着项目变大,启动越来越慢,热更新(HMR)也越来越卡。
1. 传统的 Webpack 做法(打包完再上菜)
Webpack 就像一个极其负责任的厨师。
- 你点了一个菜,它必须把整个厨房所有食材(所有 .js, .vue, .css)都洗好、切好、炒好,打包成一个巨大的 bundle.js。
- 即使你只看首页,它也要把“个人中心”、“设置页”的代码全处理一遍。
- 痛点:项目越大,等待时间越长。
2. Vite 的做法(自助餐/回转寿司)
Vite 利用了现代浏览器原生支持 ES Module (ESM) 的能力。
- 按需加载:Vite 启动时,不做打包。它只启动一个简单的服务器。
- 当你打开浏览器访问首页时,浏览器发现需要 App.vue,于是向服务器发请求:“给我 App.vue”。
- Vite 收到请求,立刻把 App.vue 编译一下发回去。
- 你没点“个人中心”,Vite 就完全不理会那部分代码。
这就是 Vite “秒开”的秘密:它把打包的工作交给浏览器去做了,且只处理你当前看到的页面。
3. 依赖预构建(Esbuild 的功劳)
那 node_modules 里的第三方库怎么办?比如 lodash 里面可能有几百个小文件,如果让浏览器一个个去请求,网络会被堵死。
这时候 Esbuild 出场了:
- Vite 会在启动前,用 Esbuild 把 node_modules 里的依赖迅速扫描并打包成几个大文件。
- 为什么快? Esbuild 是用 Go 语言写的,它是编译型语言,利用多线程并行处理。相比之下,Webpack 是用 JavaScript 写的(单线程解释执行)。
- 差距:Esbuild 比 Webpack 快 10-100 倍。
简单说:Go 语言让 Esbuild 有了「底层性能天花板」,而架构设计让它把这个天花板发挥到了极致,相比之下 Webpack 基于 JS 生态,既要兼容海量插件,又要处理复杂的打包逻辑,天然有性能瓶颈。
Vite 比 Webpack dev 快的关键:不止用了 Esbuild,更是「策略重构」
Webpack dev 模式的核心问题是:启动时会「全量打包」所有模块(包括依赖),哪怕只改一行代码,也可能重新打包整个项目;而 Vite 完全颠覆了这个逻辑:
1. 开发模式「不打包」,依赖浏览器原生 ESM
Vite 在 dev 时不会像 Webpack 那样把所有代码打包成一个 / 多个 bundle,而是:
- 依赖预构建:启动时用 Esbuild 快速处理
node_modules里的第三方依赖(如 Vue、React)—— 这些依赖通常是 CommonJS/UMD 格式,Esbuild 会把它们转成 ESM 并合并,避免浏览器频繁请求小文件; - 源码按需加载:业务代码直接以 ESM 格式交给浏览器,浏览器通过 `` 自动解析
import,只有访问某个页面时,才会请求对应的源码文件,实现「按需编译」。
而 Webpack dev 不管依赖还是源码,启动时都会全部打包,项目越大,启动越慢。
2. 热更新(HMR)更高效
- Vite 的 HMR 只更新「修改的模块」:因为源码是 ESM 格式,修改一个文件后,Vite 只需重新编译该文件,然后通知浏览器替换这个模块,无需刷新页面或重新打包;
- Webpack 的 HMR 依赖「模块热替换运行时」,且因为是打包后的 bundle,修改一个模块可能触发整个 chunk 的重新打包,耗时更长。
3. Esbuild 仅用于「特定环节」,不做全流程
Vite 只把 Esbuild 用在「依赖预构建」「TS/JSX 转译」这些高频、简单的场景,复杂的场景(如生产打包)仍用 Rollup(而非 Esbuild)—— 既利用了 Esbuild 的快,又避免了 Esbuild 功能不全的问题;而 Webpack 从转译到打包全流程都基于 JS 工具链,整体耗时更高。
Vite dev 快的核心是「双重优势」:
- 工具层面:Esbuild 基于 Go 语言 + 极致架构设计,比 Webpack 依赖的 JS 工具链(Babel、Terser 等)快一个量级;
- 策略层面:利用浏览器原生 ESM 实现「按需编译」,避免了 Webpack 「全量打包」的核心性能瓶颈。
Go 语言是 Esbuild 快的基础,但如果没有 Vite 的 ESM 按需编译策略,仅靠 Esbuild 也无法实现如此大的性能提升 —— 两者缺一不可。
第二部分:生产环境(Build)为什么用 Rollup?
好了,到了上线打包(npm run build)的时候,Vite 并没有继续用 Esbuild,而是切换回了 Rollup。
你可能会问:“既然 Esbuild 那么快,为什么打包不也用 Esbuild,而要换成 Rollup?”
1. Rollup 并不是为了“构建速度”快
老实说,Rollup 的构建速度比 Webpack 快一些,但没快得那么夸张(毕竟都是 JS 写的)。Vite 选择 Rollup 的核心原因是:Rollup 打包出来的代码(产物)跑得更快,体积更小。
2. Rollup 的杀手锏:Tree Shaking(摇树优化)
Rollup 是业界公认的 Tree Shaking 鼻祖。
举个生活例子:
假设你要搬家(打包上线),你的房间里有很多杂物(无用的代码)。
- Webpack(早期版本) :把整个房间直接装进箱子,不管那破烂还能不能用,先装走再说。
- Rollup:它会把这一堆东西拎起来“摇一摇”(Shake)。没粘在树上的枯叶(没用到的函数、变量)就直接掉地上了,只把绿叶子(有用的代码)装进箱子。
Rollup 基于 ESM 的静态结构分析,能非常精准地剔除死代码。虽然现在 Webpack 也在做,但 Rollup 的产物通常更干净、更扁平。
3. 作用域提升(Scope Hoisting)
Rollup 会尽量把所有模块的代码都放在同一个函数作用域里,而不是像 Webpack 那样给每个模块包一层函数。
- 好处:代码体积更小(少了很多函数声明的废话),浏览器执行时的内存开销也更小。
4. 为什么不用 Esbuild 打包?
目前 Esbuild 虽然快,但在代码分割(Code Splitting)和CSS 处理等高级功能上,还不如 Rollup 成熟和灵活。为了保证线上代码的稳定性和极致的小体积,Vite 选择了稳健且产物优秀的 Rollup。
总结:Vite 的“快”是组合拳
Vite 之所以强,是因为它是个“缝合怪”(褒义),它在不同的阶段选用了最合适的工具:
| 阶段 | 场景 | 使用工具 | 为什么快/好? |
|---|---|---|---|
| 开发环境 (npm run dev) | 解析项目源码 | 无 (Native ESM) | 根本不打包,浏览器按需请求,0等待。 |
| 解析 node_modules | Esbuild (Go语言) | 多线程并行,比 JS 快100倍。 | |
| 生产环境 (npm run build) | 最终打包上线 | Rollup | Tree Shaking 强,产物最干净、体积最小,加载最快。 |
一句话总结:
Vite 开发快是因为偷懒不打包(靠浏览器) + Esbuild 暴力加速;
Vite 打包用 Rollup 是因为 Rollup 像个洁癖,能把你的代码清理得最干净、体积最小。
vue项目解析vite打包
pnpm+vite创建的vue项目,node_moduels如下
esbuild和rollup被安装到了vite文件夹种,里面的代码是这样的
上面代码的解释:
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") # 获取当前脚本所在的目录
# ... 处理 Windows/Cygwin 的路径兼容性 ...
# 下面这一大坨是核心:设置 NODE_PATH
# 它的作用是告诉 Node.js:“如果你在这个脚本里 require() 了什么东西,
# 请去 .pnpm 这个仓库深处去找依赖,不要只在当前目录找。”
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/.../.pnpm/.../node_modules:..."
else
# ...
fi
# 最后一步:执行真正的文件
# "$basedir/../../../../../..." 指向了 .pnpm 仓库里真实的 esbuild 可执行文件
"$basedir/../../../../../esbuild@0.25.12/node_modules/esbuild/bin/esbuild" "$@"
从.pnpm里面找到esbuild,
这时候发现.pnpm里面还有babel相关,vue不是不靠babel编译吗,靠自己vue-complier编译器,把代码转成AST-生成redener函数,这是为什么呢
这是最关键的问题。之前说 Vite 默认不用 Babel 进行代码转译(Transpilation) (即把 ES6 转 ES5),这是事实。
但是, “不用 Babel 转译” 不代表 “不依赖 Babel 的任何工具” 。
你在 .pnpm 里看到的 Babel,通常是 @babel/parser 或 @babel/types,它们是作为工具库被引入的,而不是用来做打包转译的。
谁把 Babel 带进来的?
在标准的 Vite + Vue 项目中,即使你没装 Babel,Babel 依然存在的最大原因是:Vue 编译器 (@vue/compiler-sfc) 需要它。
让我们看看依赖链:
你的项目 -> 依赖 vue -> 依赖 @vue/compiler-sfc -> 依赖 @babel/parser
Vue 拿 Babel 干什么?
Vue 的编译器虽然自己负责解析 ,但对于 里的 JavaScript/TypeScript 代码,Vue 并没有自己写一个全套的 JS 解析器(那太难了,也没必要重复造轮子)。
Vue 使用 @babel/parser 来“看懂”你的代码,而不是“改写”你的代码。
举个例子,当你写:
defineProps({ msg: String })
const x = 1
Vue 的编译器需要分析这段代码的 AST(又是抽象语法树!) ,来搞清楚:
- 你有没有用 defineProps?
- 你声明了哪些变量(以便在 template 里可以直接用,不需要 this.)?
为了分析这些语法,Vue 引用了 Babel 的解析器。它只用 Babel 来“读”你的代码生成 AST,以便提取信息,但最后生成代码和打包时,依然是 Esbuild 和 Rollup 在干活。
还有谁可能引入 Babel?
除了 Vue 编译器,以下情况也会导致 node_modules 里出现 Babel:
- ESLint: 如果你的项目有 ESLint,它的解析器通常依赖 Babel 的相关库。
- Jest / Vitest: 单元测试工具通常重度依赖 Babel。
- Vite 插件: 某些插件可能为了方便处理代码,引入了 Babel 的工具函数
总结
-
那两个脚本:是 PNPM 生成的“快捷方式”,为了让你可以方便地调用深藏在 .pnpm 目录里的 esbuild 和 rollup。
-
为什么有 Babel:
- 并不是用来做“ES6 转 ES5”这种繁重的打包工作的。
- 主要是 Vue 编译器 (@vue/compiler-sfc) 请它来做“外包”。
- Vue 用 Babel 的解析器来读取和理解你的 内容(分析语法),仅此而已。
所以,你的 Vite 项目依然是“纯净”的现代构建,Babel 只是作为一个幕后的分析工具默默存在,并没有拖慢你的构建速度。