vite打包为什么比webpack快?

158 阅读9分钟

要回答清楚,我们需要把 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 快的核心是「双重优势」:

  1. 工具层面:Esbuild 基于 Go 语言 + 极致架构设计,比 Webpack 依赖的 JS 工具链(Babel、Terser 等)快一个量级;
  2. 策略层面:利用浏览器原生 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_modulesEsbuild (Go语言)多线程并行,比 JS 快100倍。
生产环境 (npm run build)最终打包上线RollupTree Shaking 强,产物最干净、体积最小,加载最快。

一句话总结:
Vite 开发快是因为偷懒不打包(靠浏览器)  + Esbuild 暴力加速
Vite 打包用 Rollup 是因为 Rollup 像个洁癖,能把你的代码清理得最干净、体积最小。

vue项目解析vite打包

pnpm+vite创建的vue项目,node_moduels如下

image.png

esbuild和rollup被安装到了vite文件夹种,里面的代码是这样的

image.png

上面代码的解释:

#!/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,

image.png

这时候发现.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(又是抽象语法树!) ,来搞清楚:

  1. 你有没有用 defineProps?
  2. 你声明了哪些变量(以便在 template 里可以直接用,不需要 this.)?

为了分析这些语法,Vue 引用了 Babel 的解析器。它只用 Babel 来“读”你的代码生成 AST,以便提取信息,但最后生成代码和打包时,依然是 Esbuild 和 Rollup 在干活。

还有谁可能引入 Babel?

除了 Vue 编译器,以下情况也会导致 node_modules 里出现 Babel:

  1. ESLint: 如果你的项目有 ESLint,它的解析器通常依赖 Babel 的相关库。
  2. Jest / Vitest: 单元测试工具通常重度依赖 Babel。
  3. Vite 插件: 某些插件可能为了方便处理代码,引入了 Babel 的工具函数

总结

  1. 那两个脚本:是 PNPM 生成的“快捷方式”,为了让你可以方便地调用深藏在 .pnpm 目录里的 esbuild 和 rollup。

  2. 为什么有 Babel

    • 并不是用来做“ES6 转 ES5”这种繁重的打包工作的。
    • 主要是 Vue 编译器 (@vue/compiler-sfc)  请它来做“外包”。
    • Vue 用 Babel 的解析器来读取和理解你的  内容(分析语法),仅此而已。

所以,你的 Vite 项目依然是“纯净”的现代构建,Babel 只是作为一个幕后的分析工具默默存在,并没有拖慢你的构建速度。