从 Webpack 迁移到 Vite 完整指南:告别漫长的启动等待
如果你维护过一两年以上的前端项目,多半经历过这样的场景:改一行样式,保存之后端起杯子等十几秒,热更新才慢悠悠地刷出来;早上第一次启动开发服务器,先去倒杯水再回来,进度条还在转。这不是你电脑的问题,而是 Webpack 基于「打包一切」的架构在大型项目上难以回避的性能瓶颈。
Vite 从 2020 年诞生后迅速流行,如今已成为 Vue、React 新项目的默认选择。本文不讲空泛的宣传语,而是带你走一遍真实的迁移路径:先理解两者为什么快慢有别,再动手把配置从 webpack.config.js 搬到 vite.config.js,最后处理那些最容易翻车的兼容细节。
一、为什么 Vite 启动这么快
要理解迁移的价值,得先明白 Webpack 慢在哪。
Webpack(以及基于它的 CRA、Vue CLI)在启动时会递归遍历整个依赖图,把入口文件引用的所有模块逐个打包生成 bundle,再起服务。项目越大、模块越多,这个遍历和打包过程就越慢,热更新也常常需要重新打包影响的 chunk。
Vite 换了一条思路:开发环境下它不打包,直接利用浏览器原生 ES Module 的能力。
- 启动时,Vite 只把入口 HTML 和它依赖的源码用 esbuild 做一次「预构建」(主要是把 CommonJS 依赖转成 ESM 并缓存到
node_modules/.vite),这一步是毫秒级的。 - 请求某个模块时,Vite 按需用 esbuild「单文件转译」后直接返回,浏览器自己完成加载。
- 热更新则只失效并重建「被编辑文件」那一条链路上的模块,粒度远小于 Webpack。
用一句话概括:Webpack 是先算好一切再上菜,Vite 是点哪道做哪道。 esbuild 本身是 Go 写的,转译速度又比 Babel(JS)快一个数量级,叠加下来差距非常明显。
生产环境 Vite 依然会用 Rollup 做真正的打包(tree-shaking、代码分割),所以开发快与产物质量并不矛盾。
二、迁移前的准备与目录调整
先把心态摆正:迁移不是无脑换命令,核心是配置翻译和依赖替换。建议按下面的顺序推进。
第一步,确认 Node 版本。Vite 5 需要 Node 18+,Vite 6/7 需要 Node 18.18+ / 20+。老项目如果还卡在 Node 14、16,先升级 Node 再谈迁移。
第二步,清理旧依赖,安装新依赖。以 React 项目为例:
# 卸载 webpack 及其插件、loader
npm uninstall webpack webpack-cli webpack-dev-server \
html-webpack-plugin css-loader style-loader babel-loader
# 安装 vite 和对应的框架插件
npm install -D vite @vitejs/plugin-react
第三步,移动 index.html。Vite 把 index.html 当作入口,要求它放在项目根目录(Vite 5 以后默认是根目录)。如果你之前是 CRA 结构,index.html 在 public/ 下,需要把它移到根目录,并删掉里面的 %PUBLIC_URL% 占位符,改用相对路径或直接从 /src 引入。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>我的应用</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
注意新增的 <script type="module" src="/src/main.jsx">,这是 Vite 找入口的关键。
三、翻译配置文件
这是迁移的核心工作。先看一份典型的 Webpack 配置:
// webpack.config.js(迁移前)
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/main.jsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
},
resolve: {
extensions: ['.js', '.jsx', '.json'],
alias: { '@': path.resolve(__dirname, 'src') },
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: { loader: 'babel-loader' },
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/,
type: 'asset/resource',
},
],
},
plugins: [
new HtmlWebpackPlugin({ template: './index.html' }),
],
devServer: { port: 3000, historyApiFallback: true },
};
对应的 Vite 配置长这样:
// vite.config.js(迁移后)
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { fileURLToPath, URL } from 'node:url';
export default defineConfig({
plugins: [react()],
resolve: {
extensions: ['.js', '.jsx', '.json'],
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
port: 3000,
historyApiFallback: true,
},
});
对照着看,你会发现大部分概念是能一一映射的:
| Webpack | Vite |
|---|---|
resolve.extensions |
resolve.extensions(同名,直接搬) |
resolve.alias |
resolve.alias(路径写法略有差异) |
babel-loader + .babelrc |
@vitejs/plugin-react(内置 JSX / Fast Refresh) |
style-loader + css-loader |
内置支持 CSS,什么都不用写 |
asset/resource(图片等) |
内置支持,直接 import 即可 |
HtmlWebpackPlugin |
自动处理根目录的 index.html |
devServer.port |
server.port |
devServer.historyApiFallback |
server.historyApiFallback |
几个易踩的坑要单独说:
- JSX 不需要 babel 配置了。
@vitejs/plugin-react默认支持 JSX 转换和 Fast Refresh,原来.babelrc/babel.config.js里的@babel/preset-react、react-refresh等都可以删掉。但如果你用了装饰器等 Babel 特有插件,需要保留对应配置。 process.env不再可用。Vite 用import.meta.env暴露环境变量,只有VITE_前缀的变量会注入到客户端代码。全局搜索替换process.env.REACT_APP_X为import.meta.env.VITE_X。- 别名路径写法不同。Vite 是 ESM 项目,
__dirname没了,要像上面那样用fileURLToPath(new URL(...)),或者直接用相对路径。 - 环境变量文件:
.env.development里的变量同样需要加VITE_前缀。
四、处理那些「非标配」依赖
真正的难点通常不在标准配置,而在项目里那些乱七八糟的 loader 和插件。逐个排查:
1. 动态 require
老代码里 CommonJS 风格的动态加载在 Vite 里会直接报错:
// 迁移前(Webpack 支持)
const icon = require(`./icons/${name}.svg`);
Vite 是基于 ESM 的,需要改成 import.meta.glob,这是 Vite 提供的批量懒加载方案:
// 迁移后
const icons = import.meta.glob('./icons/*.svg', { eager: true });
function getIcon(name) {
return icons[`./icons/${name}.svg`]?.default;
}
eager: true 表示启动时全部加载;去掉 eager 则返回懒加载函数,按需 import()。
2. Sass / Less 等预处理器
Vite 内置了对 .scss、.less、.styl 的识别,但对应的预处理器(sass、less、stylus)需要自己装:
npm install -D sass
装完即可,.scss 文件无需任何配置就能 import。额外的全局变量注入可以通过 css.preprocessorOptions 配置。
3. SVG as React Component
原来用 @svgr/webpack 把 SVG 当组件用,迁移到 Vite 可以改用 vite-plugin-svgr:
npm install -D vite-plugin-svgr
import svgr from 'vite-plugin-svgr';
export default defineConfig({
plugins: [svgr(), react()],
});
4. Webpack 特有的 ProvidePlugin、DefinePlugin
DefinePlugin(注入全局常量)→ 用define配置项。ProvidePlugin(自动引入全局变量,比如让所有文件都能直接用$)→ 没有直接等价物,一般建议改成显式 import,更清晰也更利于 tree-shaking。
5. 代理配置
devServer.proxy 翻译成 server.proxy,写法几乎一样:
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
}
五、验证与收尾
迁移完先别急着合并,按清单自测一遍:
npm run dev能秒开,热更新正常(试试改 CSS,看是否即时生效)。- 所有路由能直接刷新(确认
historyApiFallback生效)。 npm run build成功,且产物体积没有异常膨胀。- 图片、字体、CSS、静态资源都能正常加载(检查浏览器 Network 面板有没有 404)。
- 环境变量在开发、生产环境下取值正确。
顺带更新 package.json 里的脚本,把原来的 react-scripts start 或 webpack-dev-server 换成 Vite:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}
vite preview 用来本地预览生产构建产物,相当于本地起一个静态服务器跑 dist,上线前可以用它快速自测。
六、总结
从 Webpack 迁到 Vite,本质是把「面向打包器」的思维换成「面向浏览器 ESM」的思维。一次迁移能带来实打实的体验提升:开发服务器启动从几十秒降到一两秒,热更新从「等一等」变成「几乎秒刷」,而生产构建依旧由 Rollup 保证质量。
迁移的节奏建议如下:先处理入口和 HTML 结构,再翻译核心配置,最后逐个击破动态 require、SVG、代理等「长尾问题」。如果项目里有大量依赖 Webpack 特性的自定义 loader 或微前端架构,不要一刀切,可以先在子应用或独立仓库上试点,跑顺了再推广。
一个忠告:迁移的收益主要集中在开发体验,而不是生产性能的飞跃。如果你的团队开发机普遍配置不错、项目也不算太大,那就别为了迁移而迁移;但如果每次启动、每次热更新都让人烦躁,那这场迁移绝对值得。