2024年3月,我负责的一个中大型React项目(约200个组件,依赖50+npm包)启动开发服务器需要45秒,修改一行代码后热更新等待8秒。团队6个人每天浪费在等待构建上的时间超过2小时。老板在周会上拍桌子:“这项目还能不能干了?”
我尝试了Webpack 5的持久化缓存、thread-loader、cache-loader,启动时间降到32秒,热更新降到5秒。但还不够。直到我试了Vite,启动时间2.1秒,热更新<100ms。这篇文章就是这次迁移的完整记录。
我们的项目技术栈:React 18 + TypeScript 5.3 + Ant Design 5 + Webpack 5.90.0。开发环境配置如下:
// webpack.dev.js - 原始配置
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = {
mode: 'development',
entry: './src/index.tsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
},
{
test: /\.(png|jpg|gif|svg)$/,
type: 'asset/resource',
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.jsx'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
plugins: [
new HtmlWebpackPlugin({ template: './public/index.html' }),
new ReactRefreshWebpackPlugin(),
],
devServer: {
port: 3000,
hot: true,
historyApiFallback: true,
},
};
压测数据(使用webpack-bundle-analyzer + 手动计时,10次取平均):
我尝试了以下优化手段:
// webpack.optimized.js - 优化后配置
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = {
mode: 'development',
entry: './src/index.tsx',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
},
// 开启持久化缓存
cache: {
type: 'filesystem',
cacheDirectory: path.resolve(__dirname, '.temp_cache'),
buildDependencies: {
config: [__filename],
},
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
use: [
{
loader: 'thread-loader',
options: {
workers: 4, // 利用多核CPU
workerParallelJobs: 50,
},
},
{
loader: 'ts-loader',
options: {
happyPackMode: true, // 与thread-loader配合
transpileOnly: true, // 仅转译,不进行类型检查
},
},
],
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.jsx'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
// 缩小模块搜索范围
modules: [path.resolve(__dirname, 'src'), 'node_modules'],
symlinks: false,
},
plugins: [
new HtmlWebpackPlugin({ template: './public/index.html' }),
new ReactRefreshWebpackPlugin(),
],
devServer: {
port: 3000,
hot: true,
historyApiFallback: true,
},
// 关闭性能提示
performance: {
hints: false,
},
};
优化后数据:
结论:Webpack优化有上限,大型项目依然慢。
Vite 5.1.0 + @vitejs/plugin-react 4.2.1。完整配置:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
server: {
port: 3000,
open: true,
// 开启HMR
hmr: {
overlay: true,
},
},
build: {
// 生产构建使用Rollup
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
antd: ['antd'],
},
},
},
// 使用ESBuild进行压缩
minify: 'esbuild',
target: 'es2020',
},
// 优化依赖预构建
optimizeDeps: {
include: ['react', 'react-dom', 'antd', '@ant-design/icons'],
exclude: ['your-local-package'], // 排除不需要预构建的包
},
css: {
// CSS模块化配置
modules: {
localsConvention: 'camelCase',
},
preprocessorOptions: {
less: {
javascriptEnabled: true,
},
},
},
});
迁移后数据:
# 卸载Webpack相关
npm uninstall webpack webpack-cli webpack-dev-server html-webpack-plugin ts-loader thread-loader
# 安装Vite相关
npm install --save-dev vite @vitejs/plugin-react vite-plugin-imp
# 确保React版本兼容(需要React 17+)
npm install react@18 react-dom@18
# 安装TypeScript支持
npm install --save-dev typescript @types/react @types/react-dom
// tsconfig.json - 适配Vite
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>React App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
// src/main.tsx - 入口文件
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// src/utils/env.ts - Vite环境变量使用方式
// Vite使用import.meta.env,而非process.env
export function getApiBaseUrl(): string {
// 开发环境
if (import.meta.env.DEV) {
return 'http://localhost:8000/api';
}
// 生产环境
return import.meta.env.VITE_API_BASE_URL || 'https://api.example.com';
}
// 使用方式
console.log(import.meta.env.VITE_APP_TITLE); // 需要在.env文件中定义VITE_前缀的变量
# .env - 环境变量文件
VITE_APP_TITLE=My React App
VITE_API_BASE_URL=https://api.example.com
/* src/styles/global.css - 全局样式 */
/* Vite支持CSS @import */
@import './variables.css';
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
/* 使用CSS Modules */
:local(.container) {
max-width: 1200px;
margin: 0 auto;
}
// src/components/Header.tsx - 使用CSS Modules
import styles from './Header.module.css';
function Header() {
return (
<header className={styles.header}>
<img src="/logo.png" alt="Logo" /> {/* 静态资源直接放public目录 */}
<h1>{import.meta.env.VITE_APP_TITLE}</h1>
</header>
);
}
export default Header;
| 指标 | Webpack 5原始 | Webpack 5优化 | Vite 5 | 提升幅度 |
|---|---|---|---|---|
| 冷启动时间 | 45.2s | 32.1s | 2.1s | 95.4% |
| 热更新(单文件) | 8.3s | 5.2s | 89ms | 98.9% |
| 增量构建 | 6.7s | 4.8s | 1.2s | 82.1% |
| 生产构建 | 52.3s | 48.6s | 18.7s | 64.2% |
| 内存占用 | 1.2GB | 1.5GB | 380MB | 68.3% |
| 构建产物大小 | 4.8MB | 4.5MB | 4.2MB | 12.5% |
测试环境:MacBook Pro M1 Pro 16GB RAM,Node.js 20.11.0,项目包含200+组件,50+依赖包。
问题:Vite中@别名在编译时正常,但TypeScript报错。解决:tsconfig.json中必须配置paths,且baseUrl要设置。
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
问题:迁移后Ant Design样式丢失。解决:使用vite-plugin-imp实现按需加载。
// vite.config.ts
import vitePluginImp from 'vite-plugin-imp';
export default defineConfig({
plugins: [
react(),
vitePluginImp({
libList: [
{
libName: 'antd',
style: (name) => `antd/es/${name}/style`,
},
],
}),
],
});
问题:使用process.env报错。Vite只暴露import.meta.env,且变量必须以VITE_开头。所有代码中process.env都要替换。
问题:修改某些文件后页面不刷新。解决:检查是否使用了错误的导出方式,Vite要求ES Module导出。避免使用module.exports。
问题:构建时出现“Unexpected token”错误。解决:检查是否使用了Node.js特有API(如fs、path),Vite构建时这些不可用。使用rollup插件处理。
Vite不是银弹,但对于中大型React项目,迁移成本低(1-2天),收益巨大。如果你的项目Webpack启动超过10秒,热更新超过2秒,建议立即迁移。记住:工具是服务人的,不是人服务工具。
专注技术分享与实战