前面几篇都在讲 webpack 的 loader 和 plugin。但如今新建项目,很多人第一反应是 Vite。于是「Vite 和 webpack 到底差在哪」成了绕不开的问题。本文不站队,只从工作原理出发,把两者的差异讲清楚。
一、先看结论
| 维度 | webpack | Vite |
|---|---|---|
| 开发启动 | 先打包,再启动服务 | 不打包,按需编译 |
| 依赖处理 | 全部进入 bundle | esbuild 预构建,浏览器原生 ESM 加载 |
| 冷启动速度 | 随项目规模线性变慢 | 基本恒定,与模块数弱相关 |
| HMR | 重建模块图后推送 | 沿 ESM 边界精确更新 |
| 生产构建 | webpack 打包 | Rollup 打包 |
| 配置风格 | 强大但繁琐 | 约定优先,开箱即用 |
| 插件机制 | Tapable 钩子 | Rollup 兼容插件 + Vite 专属钩子 |
| 浏览器兼容 | 可兼容旧浏览器 | 开发阶段需原生 ESM |
| dev/prod | 同一套引擎,一致性高 | 开发 ESM、生产 Rollup,可能有细微差异 |
一句话:webpack 是「先打包,再运行」,Vite 是「先运行,用到再编译」。
二、webpack 是怎么工作的
2.1 一切皆模块,先打包再运行
webpack 的核心模型是依赖图:从 entry 出发,递归解析 import / require,把所有模块(JS、CSS、图片……)经过 loader 转换后,拼装成一个或多个 bundle。
浏览器不认识 import './style.css',也不认识裸模块名 import Vue from 'vue'。这些都需要 webpack 在构建期解析、替换、合并,最终产出一份浏览器能直接执行的 JS。
所以 webpack 的产物必须「先构建完成」才能运行。
2.2 dev server 也在打包
启动 webpack-dev-server 时,webpack 并没有偷懒:它照样跑一遍完整构建,只是把产物放在内存里而不是磁盘。浏览器请求 bundle.js,dev server 直接返回内存里的结果。
// webpack.config.js
module.exports = {
entry: './src/main.js',
devServer: { hot: true, port: 8080 },
};
这就解释了一个现象:项目越大,npm run dev 越慢——因为服务的启动时间几乎等于一次完整打包的时间。webpack 5 的持久化缓存(cache: { type: 'filesystem' })能缓解二次启动,但首次启动依然要全量构建。
2.3 HMR 的代价
修改一个文件后,webpack 需要在依赖图里找出受影响的模块并重新构建相关 chunk,再把更新推给浏览器。依赖图越大,这个「找影响范围 + 重建」的过程越慢。
三、Vite 是怎么工作的
3.1 开发:不打包,按需编译
Vite 开发时直接把源码交给浏览器。因为现代浏览器原生支持 ESM,<script type="module" src="/src/main.js"> 就能工作。当浏览器执行到 import 时,会向 dev server 发请求,Vite 这时才即时编译这一个文件并返回。
浏览器 → 请求 /src/main.js → Vite 编译并返回
→ 遇到 import './App.vue' → 再请求 → Vite 编译该文件
也就是说,只有被真正请求到的模块才会被编译。页面首屏用了多少模块,就编多少模块,与项目总规模无关——这就是 Vite 冷启动快的根本原因。
3.2 依赖预构建:解决「裸模块」问题
浏览器不认识 import Vue from 'vue' 这种裸模块名,而且一个依赖往往内部有几十上百个文件,逐个请求会造成请求风暴。
Vite 的做法是用 esbuild 先做一次「依赖预构建」:
- 把 CommonJS / UMD 依赖转成 ESM;
- 把零散的内部模块合并成一个文件,减少请求数;
- 结果缓存在
node_modules/.vite,依赖不变就复用。
esbuild 用 Go 编写,比 JS 实现的打包器快一到两个数量级,所以这一步通常很快。
3.3 生产:换成 Rollup
开发阶段不打包是为了快,但生产环境需要 tree-shaking、代码分割、压缩等优化,所以 Vite 在生产构建时换用 Rollup:
vite build # 底层是 Rollup
这也是 Vite 与 webpack 的一个重要差异:开发和生产的底层引擎不是同一个。
四、核心差异逐项看
4.1 启动速度
- webpack 的启动成本 = 一次完整打包,与项目规模正相关。
- Vite 的启动成本 = 启动服务 + 依赖预构建,与业务代码规模基本无关。
项目越大,差距越明显。小项目里两者体感接近。
4.2 热更新 HMR
- webpack:文件改动后,沿依赖图反向查找受影响的模块,重新构建对应部分,通过 HMR runtime 推送到浏览器。模块图越大,定位越慢。
- Vite:利用 ESM 的模块边界,只让改动模块及其直接引用者失效,请求该模块时重新编译。更新范围小且精确。
Vite 的 HMR 通常更快,但两者在现代项目中都能达到「毫秒级」体感,webpack 的差距在大项目里才明显。
4.3 生产构建
| webpack | Vite | |
|---|---|---|
| 引擎 | webpack | Rollup |
| Tree-shaking | 支持(依赖 ESM) | 支持,Rollup 更彻底 |
| 代码分割 | 成熟,策略丰富 | 支持,开箱即用 |
| 产物优化 | 插件生态极丰富 | 依赖 Rollup 插件 |
| Module Federation | 原生支持 | 需第三方插件,支持有限 |
生产构建上 webpack 依旧强势:Module Federation(模块联邦)、复杂分包策略等高级能力,webpack 的生态更成熟。
4.4 配置风格
webpack 的配置是出了名的多:
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: './src/main.js',
output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[hash].js', clean: true },
module: {
rules: [
{ test: /\.js$/, exclude: /node_modules/, use: 'babel-loader' },
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
{ test: /\.(png|svg)$/, type: 'asset/resource' },
],
},
plugins: [new HtmlWebpackPlugin({ template: './index.html' })],
devServer: { hot: true },
};
同样的项目,Vite 的配置是:
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
});
Vite 把大量约定内置(入口 index.html、CSS/图片/TS 开箱可用),配置量显著减少。代价是想深度定制时,webpack 的显式配置反而更可控。
4.5 插件机制
- webpack:基于 Tapable 的钩子系统,插件可以介入从启动到产物落盘的任意阶段,控制粒度极细,但学习曲线陡。
- Vite:兼容大部分 Rollup 插件接口,同时提供 Vite 专属钩子(
config、transformIndexHtml、handleHotUpdate、configureServer等)。写起来更简单,但开发态与构建态钩子的语义差异需要留意。
如果你熟悉 webpack 的 loader,可以把 Vite 的插件理解为「Rollup 插件 + dev server 增强」的组合。
4.6 浏览器兼容
- webpack:产物是普通 bundle,配合 Babel 可以兼容老浏览器。
- Vite:开发态依赖浏览器原生 ESM,新项目开发默认面向现代浏览器;生产产物可以用
@vitejs/plugin-legacy转译以兼容旧浏览器。
如果必须支持 IE 或很旧的移动端浏览器,webpack 更省心。
4.7 dev/prod 一致性
这是 Vite 常被讨论的一点:
- webpack 开发和构建都用同一套引擎,行为基本一致;
- Vite 开发用 esbuild + 原生 ESM,生产用 Rollup,两个引擎可能产生细微差异(如 CSS 处理顺序、
import.meta行为等),偶尔出现「开发正常、构建出错」。
Vite 团队一直在收敛这些差异,但作为使用者需要知道这个前提。
五、到底该选谁
| 场景 | 推荐 |
|---|---|
| 新项目、现代浏览器、追求开发体验 | Vite |
| Vue / React / Svelte 等标准 SPA | Vite |
| 需要兼容旧浏览器 | webpack |
| 需要 Module Federation、微前端 | webpack |
| 深度定制构建流程、复杂 loader 链 | webpack |
| 老项目已有完整 webpack 配置 | 继续 webpack,或渐进迁移 |
| 库开发(需要精细产物控制) | 两者皆可,Rollup / tsup 也常见 |
一个务实的态度:Vite 解决的是「开发体验」,webpack 解决的是「构建能力上限」。大多数业务项目 Vite 足够;需要复杂构建编排时,webpack 仍是更稳的选择。
六、小结
- webpack:先打包再运行,依赖图 + loader + Tapable 插件,配置强大、生态成熟,开发启动随规模变慢。
- Vite:开发不打包,按需编译,用 esbuild 预构建依赖、浏览器原生 ESM 加载;生产用 Rollup。
- 核心差异在开发模式:webpack 全量构建,Vite 按需编译。
- 选择取决于场景:追求开发体验选 Vite,需要兼容性与复杂构建能力选 webpack。
理解了两者的模型差异,再回头看 webpack 的 loader 和 plugin 机制,你会发现它们解决的是同一类问题的两种思路——这也是这个系列先讲 webpack 的原因。