Vite 与 Webpack 的根本区别
要理解 Vite 为什么快,首先要理解 Webpack 为什么慢。Webpack 的工作方式是:从入口文件出发,递归地解析所有依赖模块,将它们打包成一个或多个 bundle,然后启动开发服务器。当你修改一个文件时,Webpack 需要重新打包受影响的模块——项目越大,这个过程越慢。
Vite 的思路完全不同:
- 开发阶段:利用浏览器原生 ESM 支持,直接将源码作为 ES 模块发送给浏览器,不做打包
- 生产构建:使用 Rollup 进行打包,享受成熟的 Tree Shaking 和代码分割
这意味着 Vite 的开发服务器启动只需要几百毫秒,无论项目多大。因为不需要打包,只是按需编译被请求的模块。
ESBuild 预构建:Vite 的第一个魔法
既然开发时不做打包,那 node_modules 里的依赖怎么办?直接加载 npm 包会遇到两个问题:
- 很多 npm 包是 CommonJS 格式,浏览器不认识
require() - 一个包可能包含成百上千个内部模块,逐个请求会引发"请求瀑布"
Vite 的解决方案是依赖预构建(Dependency Pre-Bundling)。在启动开发服务器之前,Vite 会用 ESBuild 扫描所有第三方依赖,将它们转换为 ESM 格式并合并成少量文件:
// 预构建前:lodash-es 被拆成 600+ 个模块请求
import { debounce } from 'lodash-es';
// 预构建后:合并为一个文件
// node_modules/.vite/deps/lodash-es.js
ESBuild 是用 Go 语言编写的,比 JavaScript 编写的打包器快 10-100 倍。它处理依赖预构建的速度快到几乎感觉不到。
原生 ESM 开发服务器
Vite 的开发服务器本质上是一个静态文件服务器,但它在返回 .js、.ts、.vue 文件时会做一个"即时编译"的操作:
// 浏览器请求 src/main.ts
// Vite 服务器:
// 1. 用 ESBuild 将 TypeScript 转换为 JavaScript
// 2. 处理 import 路径重写(裸模块 -> 预构建缓存路径)
// 3. 注入 HMR 客户端代码
// 4. 返回给浏览器
关键点在于:Vite 只在浏览器请求某个模块时才编译它,而不是一次性编译整个项目。这被称为"按需编译"(on-demand compilation)。
// Vite 会重写这样的 import
import { ref } from 'vue'
// 变成
import { ref } from '/node_modules/.vite/deps/vue.js?v=abc123'
HMR 实现原理
Vite 的 HMR(热模块替换)基于原生 ESM,实现比 Webpack 简洁得多。核心流程是:
- Vite 在开发服务器和浏览器之间建立 WebSocket 连接
- 文件变更时,Vite 只失效该模块及其直接依赖者
- 浏览器通过 WebSocket 收到更新通知,重新请求变更的模块
- 新的模块替换旧的模块,应用状态得以保留
// Vite 注入的 HMR 客户端代码(简化版)
if (import.meta.hot) {
import.meta.hot.accept('./component.js', (newModule) => {
// 用新模块替换旧模块
});
}
这种 HMR 的速度与模块数量无关,只与变更的模块本身有关。在大型项目中,Vite 的 HMR 可以做到 50ms 以内完成更新,而 Webpack 可能需要数秒。
Rollup 生产构建
Vite 的生产构建使用 Rollup,而不是 ESBuild。原因是:
- Rollup 的插件生态极其成熟,几乎覆盖所有构建需求
- Rollup 生成的代码更干净,Tree Shaking 效果更好
- ESBuild 虽然快,但在代码分割、CSS 处理方面还不够成熟
Vite 的构建配置基本等同于 Rollup 配置,大多数插件可以直接使用:
// vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
// 直接传递 Rollup 配置
output: {
manualChunks: {
vendor: ['vue', 'vue-router'],
},
},
},
},
});
插件系统概览
Vite 的插件系统兼容 Rollup 插件接口,同时扩展了 Vite 特有的钩子。一个 Vite 插件的基本结构:
export default function myPlugin() {
return {
name: 'my-plugin',
// Rollup 钩子:在构建阶段调用
transform(code, id) {
if (id.endsWith('.custom')) {
return { code: transformCustom(code), map: null };
}
},
// Vite 特有钩子:在开发服务器阶段调用
configureServer(server) {
server.middlewares.use((req, res, next) => {
// 自定义中间件
next();
});
},
};
}
常用的 Vite 插件包括:@vitejs/plugin-vue、@vitejs/plugin-react、vite-plugin-pwa、unplugin-auto-import 等。
从 Webpack 迁移到 Vite
迁移的难度取决于你的项目复杂度,但大多数情况下可以分步进行:
- 安装 Vite 和对应的框架插件
- 创建
vite.config.js,将 Webpack 配置逐项翻译 - 修改
index.html位置(Vite 要求它在项目根目录) - 替换环境变量:
process.env.X改为import.meta.env.VITE_X - 替换
require()为import语句 - 调整别名:
resolve.alias语法略有不同
// Webpack 配置
module.exports = {
resolve: {
alias: { '@': path.resolve(__dirname, 'src') },
},
};
// Vite 配置
export default defineConfig({
resolve: {
alias: { '@': '/src' },
},
});
迁移建议:不要一次性迁移整个项目。先在一个新分支上尝试,遇到问题逐个解决。大多数中小型项目可以在 1-2 天内完成迁移。
总结
Vite 的成功不是偶然的。它精准地抓住了 Webpack 的痛点——开发体验慢——并用"原生 ESM + 按需编译"这个优雅的方案解决了它。ESBuild 做预构建保证了冷启动速度,Rollup 做生产构建保证了产物质量,两者的结合让 Vite 同时拥有了极致的开发体验和可靠的生产构建。如果你还在用 Webpack 做新项目,不妨试试 Vite——你大概率不会再想回去。