2024 年我们的 React 前端单页应用从 Webpack 4 升到 Webpack 5,代码量从 30 万行涨到 80 万行,构建时间从 3 分钟变成 9 分钟,本地 dev server 启动要 50 秒,HMR 也慢。投了三周做构建性能治理,最终生产构建 9min → 1.5min,dev 启动 50s → 4s,HMR 8s → 200ms。本文复盘 Webpack/Vite/Turbopack 构建性能优化的完整实战,覆盖测量、分包、缓存、并行、迁移决策。
问题背景
应用:React 18 + TS + Webpack 5
代码量:80w 行 TS + 5000 个组件
依赖:1200 个 npm 包(node_modules 1.8GB)
路由:200+ 路由
性能问题:
- 生产 build:9 分钟
- 本地 dev server 启动:50 秒
- HMR 改一个组件:8 秒
- vendor.js 大小:6.8MB(gzipped 1.8MB)
- 首页 FCP 4.2s
CI 影响:
- 一次 PR build + 测试:25 分钟
- 主干合并后 deploy 慢
- 前端开发人员 50% 时间在等编译
第 1 步:测量(找瓶颈)
# 1. speed-measure-webpack-plugin
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
// ... 你的 webpack 配置
});
# 输出
SMP ⏱
General output time took 6 mins, 32.5 secs
SMP ⏱ Plugins
TerserPlugin took 2 mins, 14 secs ← 占 35%
HtmlWebpackPlugin took 12 secs
CopyPlugin took 8 secs
SMP ⏱ Loaders
babel-loader took 1 mins, 45 secs ← 占 28%
ts-loader took 1 mins, 30 secs
sass-loader took 22 secs
modules with no loaders took 18 secs
# 2. webpack-bundle-analyzer 分析包体积
$ npx webpack-bundle-analyzer dist/stats.json
# 找到:
# - moment.js + locales: 350KB(其实只用 en-US)
# - lodash 全量引入: 540KB(只用 5 个函数)
# - antd 4 没按需:1.2MB
# - react + react-dom:200KB
# - 大文件 echarts(800KB)、xlsx(600KB)
# 3. webpack --profile --json > stats.json
# 上传到 https://webpack.github.io/analyse/ 可视化
第 2 步:缓存(persistentCache)
// webpack.config.js
module.exports = {
cache: {
type: 'filesystem', // Webpack 5 持久化缓存
cacheDirectory: path.resolve(__dirname, '.webpack-cache'),
buildDependencies: {
config: [__filename], // config 改了才失效
},
compression: 'gzip',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 天
},
// Babel 缓存
module: {
rules: [
{
test: /\.tsx?$/,
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true, // 必开
cacheCompression: false,
},
},
],
},
],
},
};
// 效果:
// 首次构建 6min
// 二次构建(代码无改动)5s ← 完全 cache
// 二次构建(改一个 component)40s
第 3 步:替换慢的 loader / plugin
// 1. babel-loader → swc-loader(快 10-70 倍)
{
test: /\.tsx?$/,
use: {
loader: 'swc-loader',
options: {
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
},
transform: {
react: {
runtime: 'automatic',
development: process.env.NODE_ENV !== 'production',
},
},
target: 'es2020',
},
},
},
}
// 2. TerserPlugin → SwcMinifyWebpackPlugin
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
// 旧:Terser(慢但稳)
// new TerserPlugin({ parallel: true })
// 新:SWC(快)
new TerserPlugin({
minify: TerserPlugin.swcMinify,
terserOptions: {
format: { comments: false },
compress: { drop_console: true },
},
}),
],
},
};
// 3. sass-loader → 用 sass-embedded(原生绑定,快 5 倍)
// package.json
{
"devDependencies": {
"sass-embedded": "^1.69.5" // 替换 sass
}
}
// 4. ts-loader → 不要做类型检查
{
test: /\.tsx?$/,
use: [
{
loader: 'swc-loader', // 只做转译
},
],
}
// 类型检查独立跑:tsc --noEmit --watch
// 或用 fork-ts-checker-webpack-plugin(独立进程并行)
第 4 步:按需引入 + Tree Shaking
// 1. lodash 按需
// 不好:540KB
import _ from 'lodash';
_.debounce(fn, 200);
// 好:只引你用的
import debounce from 'lodash/debounce';
debounce(fn, 200);
// 更好:lodash-es(原生 ES Module,tree-shake 友好)
import { debounce } from 'lodash-es';
// 2. moment.js 替换为 dayjs
// moment + locales: 350KB
// dayjs: 7KB
import dayjs from 'dayjs';
dayjs().format('YYYY-MM-DD');
// 3. antd 按需(antd 5 默认支持)
import { Button, Table } from 'antd';
// 不需要 babel-plugin-import,antd 5 自带 ES Module
// 4. 自己代码也要 sideEffects: false
// package.json
{
"name": "@my/utils",
"sideEffects": false, // 或 ["*.css"]
"main": "dist/index.cjs.js",
"module": "dist/index.esm.js"
}
// 验证:webpack --json | grep "tree-shaking"
// 或看 stats:webpack-bundle-analyzer 里有 unused module 警告
第 5 步:并行 + 多线程
// 1. thread-loader(已过时,改用 swc-loader 内置并行)
// SWC 自动多核
// 2. TerserPlugin 并行
new TerserPlugin({
parallel: true, // 默认 os.cpus().length - 1
minify: TerserPlugin.swcMinify,
})
// 3. fork-ts-checker-webpack-plugin
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
plugins: [
new ForkTsCheckerWebpackPlugin({
typescript: {
mode: 'write-references',
memoryLimit: 4096,
diagnosticOptions: {
syntactic: true,
},
},
async: true, // 不阻塞构建
}),
]
// 4. CI 多机并行(turborepo / nx)
// 60 个包分到 5 台机器,每台跑 12 个
第 6 步:分包优化
// SplitChunksPlugin 配置
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
maxAsyncRequests: 30,
maxInitialRequests: 30,
minSize: 20000,
cacheGroups: {
// 1. React 全家桶单独
react: {
test: /[\\/]node_modules[\\/](react|react-dom|react-router|react-router-dom)[\\/]/,
name: 'react',
chunks: 'all',
priority: 40,
},
// 2. antd 单独(大依赖)
antd: {
test: /[\\/]node_modules[\\/]antd[\\/]/,
name: 'antd',
priority: 30,
},
// 3. echarts 单独(用得少但大)
echarts: {
test: /[\\/]node_modules[\\/]echarts[\\/]/,
name: 'echarts',
priority: 25,
},
// 4. 其他 vendor
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all',
priority: 10,
},
// 5. 业务公共模块
common: {
minChunks: 2,
chunks: 'all',
priority: 5,
reuseExistingChunk: true,
},
},
},
// contenthash 利于缓存
runtimeChunk: 'single',
moduleIds: 'deterministic',
},
};
// 路由级别 code splitting
const HomePage = React.lazy(() => import(/* webpackChunkName: "home" */ './pages/Home'));
const AdminPage = React.lazy(() => import(/* webpackChunkName: "admin" */ './pages/Admin'));
// 效果:
// - vendor.js 6.8MB → react.js 130KB + antd.js 800KB + echarts.js 700KB + vendor.js 1.2MB
// - 首页只加载 react + 业务 main(总 800KB),不需要的 chunk 路由切换时再加载
// - CDN 缓存友好,React 不变的话用户复用
第 7 步:考虑 Vite / Turbopack
# Vite 开发体验秒杀 Webpack(原生 ESM + esbuild)
# 适合:新项目 / 中小型项目 / 不依赖复杂 Webpack 配置
# 我们项目尝试 Vite 迁移
$ npm create vite@latest my-app -- --template react-ts
# vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc'; // SWC 版
export default defineConfig({
plugins: [react()],
build: {
target: 'es2020',
rollupOptions: {
output: {
manualChunks: {
react: ['react', 'react-dom'],
antd: ['antd'],
echarts: ['echarts'],
},
},
},
},
server: {
port: 3000,
proxy: {
'/api': 'http://localhost:8080',
},
},
});
# 实测:
# 启动:50s → 4s
# HMR:8s → 200ms
# 生产 build:9min → 4min(Rollup 还是慢,但能接受)
# Turbopack(Next.js 团队搞的,Rust 实现)
# 目前还在 beta,适合 Next.js 项目
$ next dev --turbo # 启动比 Vite 还快
# 迁移决策:
# - 老项目 Webpack 配置复杂(自定义 loader / plugin),坚守 Webpack + 优化
# - 中小项目,可以迁 Vite
# - Next.js 项目,试 Turbopack
CI 流水线优化
name: CI
on: pull_request
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
# 关键:cache .webpack-cache 目录
- uses: actions/cache@v4
with:
path: |
.webpack-cache
node_modules/.cache
key: webpack-${{ runner.os }}-${{ hashFiles('package-lock.json', 'webpack.config.js') }}-${{ github.sha }}
restore-keys: |
webpack-${{ runner.os }}-${{ hashFiles('package-lock.json', 'webpack.config.js') }}-
webpack-${{ runner.os }}-
- run: pnpm install --frozen-lockfile
# 并行 typecheck + build + test
- run: pnpm run typecheck &
pnpm run build &
pnpm run test &
wait
# 产物上传
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
# 效果:CI 时长 25min → 4min(首次)/ 1min(全 cache 命中)
优化效果
指标 优化前 优化后
=========================================================
生产 build 时间 9min 1.5min
首次 dev 启动 50s 8s(Webpack)/ 4s(Vite)
增量 HMR 8s 200ms(Vite)
vendor.js 大小 6.8MB react 130KB + antd 800KB + vendor 800KB
gzipped 总大小 1.8MB 550KB
首屏 FCP 4.2s 1.6s
首屏 LCP 5.8s 2.3s
CI 时长
首次构建 25min 4min
全 cache 命中 -- 1min
业务影响:
- 前端开发体验大幅改善
- 首屏加载快 3 倍,转化率 +5%
- 移动端弱网友好(550KB vs 1.8MB)
- PR 反馈速度快,迭代效率高
避坑清单
- 先测量(speed-measure + bundle-analyzer)再优化
- 持久化缓存(cache.type='filesystem')必开
- babel-loader → swc-loader,提速 10-70 倍
- Terser → swcMinify,压缩快 3-5 倍
- lodash / moment / antd 按需引入,sideEffects: false
- 路由级 code splitting + 大依赖单独分包
- ts-loader 不做类型检查,fork-ts-checker 独立跑
- CI cache .webpack-cache + node_modules/.cache 目录
- 中小项目可考虑迁 Vite,HMR 快 40 倍
- contenthash + runtimeChunk single,利于 CDN 缓存
总结
Webpack 构建性能优化是个工具替换 + 配置调整的过程:每一步都有明显收益。最大的认知改变:Webpack 5 的 filesystem 缓存被严重低估,首次配上,二次构建几乎瞬间完成,这一条就能把 CI 时间砍掉 60%。其次是 SWC 替代 Babel 和 Terser,Rust 实现的工具链速度是 JS 的 10-70 倍,对大型项目立竿见影。最容易踩的坑是按需引入做不彻底:import _ from 'lodash' 一行就把 540KB 拉进 bundle,改成具体函数 import 立省一半。最被低估的是 sideEffects: false,这一行让 webpack 敢做激进 tree-shaking,业务库自己也要标。最后,如果是新项目或中小项目,2024 年直接用 Vite,不要再纠结 Webpack 配置;但老项目 Webpack 用得很深的话,优化到位也能拿到 5-10 倍提速,不必激进迁移。
—— 别看了 · 2026