Webpack 4 已經相當成熟,但生產環境的優化配置依然讓人頭疼。這篇文章整理了我們團隊在實際項目中使用的 Webpack 優化清單,覆蓋壓縮、Tree Shaking、代碼分割、持久緩存、體積分析等環節。每個優化點都給出可直接使用的配置代碼。
代碼壓縮
Webpack 4 的 mode: 'production' 默認開啓 TerserPlugin 壓縮 JS。但我們可以進一步調整配置:
javascript
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = {
mode: 'production',
optimization: {
minimizer: [
// JS 壓縮
new TerserPlugin({
parallel: true, // 多進程並行壓縮
cache: true, // 開啓緩存(Webpack 5 無需此配置)
terserOptions: {
compress: {
drop_console: true, // 移除 console.log
drop_debugger: true, // 移除 debugger
passes: 2, // 壓縮遍歷次數
},
output: {
comments: false, // 移除註釋
},
},
extractComments: false, // 不提取 license 到單獨文件
}),
// CSS 壓縮
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
safe: true,
discardComments: { removeAll: true },
},
}),
],
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
}),
],
}
Tree Shaking 驗證
Tree Shaking 在 mode: 'production' 下自動開啓,但要確保它真正生效,需要注意幾個條件:
javascript
// 條件一:使用 ES Module 導出(不要混用 CommonJS)
// utils.js - 正確
export function add(a, b) { return a + b }
export function subtract(a, b) { return a - b }
export function multiply(a, b) { return a * b }
// 錯誤寫法(Tree Shaking 無法生效)
// module.exports = { add, subtract, multiply }
// 條件二:按需導入
import { add } from './utils'
// 而不是
import * as utils from './utils'
// 條件三:在 package.json 中標記 sideEffects
// package.json
{
"name": "my-project",
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfills.js"
]
// 如果沒有任何副作用,可以設為 false
// "sideEffects": false
}
驗證 Tree Shaking 是否生效的方法:
bash
# 方法一:使用 Webpack Bundle Analyzer 查看
npm install --save-dev webpack-bundle-analyzer
# 方法二:搜索打包產物中的死代碼
# 如果 multiply 函數沒有被使用,打包產物中不應包含它
grep "multiply" dist/main.js
# 方法三:使用 --display-used-exports 查看
npx webpack --mode production --display-used-exports
代碼分割策略
代碼分割是優化加載性能最有效的手段。我們採用多層級的分割策略:
javascript
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
minSize: 30000, // 模塊超過 30KB 才分割
maxSize: 244000, // 超過 244KB 進一步拆分
minChunks: 1,
maxAsyncRequests: 6, // 並行加載的最大請求數
maxInitialRequests: 4, // 入口最大並行請求數
automaticNameDelimiter: '~',
cacheGroups: {
// 核心框架單獨打包(變化頻率最低)
vendors: {
test: /[\\/]node_modules[\\/](react|react-dom|react-router)[\\/]/,
name: 'vendors',
priority: 30,
chunks: 'all',
reuseExistingChunk: true,
},
// 其他第三方庫
commons: {
test: /[\\/]node_modules[\\/]/,
name: 'commons',
priority: 20,
chunks: 'all',
reuseExistingChunk: true,
},
// 公共模塊
shared: {
name: 'shared',
minChunks: 2, // 被至少 2 個 chunk 引用
priority: 10,
reuseExistingChunk: true,
},
// CSS 單獨打包
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'all',
enforce: true,
},
},
},
// 提取 Webpack 運行時,避免 vendors hash 變化
runtimeChunk: {
name: 'runtime',
},
},
}
路由級別的動態導入:
javascript
// 路由懶加載
import React, { Suspense, lazy } from 'react'
// 不要寫成這樣(所有頁面打包在一起)
// import Home from './pages/Home'
// import Dashboard from './pages/Dashboard'
// 使用動態導入實現按需加載
const Home = lazy(() => import(/* webpackChunkName: "home" */ './pages/Home'))
const Dashboard = lazy(() => import(/* webpackChunkName: "dashboard" */ './pages/Dashboard'))
const Settings = lazy(() => import(/* webpackChunkName: "settings" */ './pages/Settings'))
const User = lazy(() => import(/* webpackChunkName: "user" */ './pages/User'))
function App() {
return (
<Suspense fallback={<div>加載中...</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/dashboard" component={Dashboard} />
<Route path="/settings" component={Settings} />
<Route path="/user/:id" component={User} />
</Switch>
</Suspense>
)
}
持久緩存
通過 contenthash 實現長期緩存,文件內容不變則 hash 不變,瀏覽器可以使用緩存:
javascript
module.exports = {
output: {
// contenthash: 基於文件內容生成 hash
// 文件沒變 -> hash 不變 -> 瀏覽器使用緩存
filename: 'js/[name].[contenthash:8].js',
chunkFilename: 'js/[name].[contenthash:8].chunk.js',
},
optimization: {
// 提取 runtime 到單獨文件
// Webpack 的運行時代碼很小但會頻繁變化
// 單獨打包可以避免 vendors 的 hash 變化
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
}
配合 HTML 模板和 CDN 部署:
javascript
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
},
// 注入 CDN 前綴(可選)
cdn: {
css: ['https://cdn.example.com/lib/antd.min.css'],
js: ['https://cdn.example.com/lib/react.production.min.js'],
},
}),
],
}
體積分析
定期分析包體積是保持項目健康的關鍵:
javascript
// webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = {
plugins: [
// 僅在 ANALYZE 環境變量下開啓
process.env.ANALYZE && new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-report.html',
openAnalyzer: false,
}),
].filter(Boolean),
}
bash
# 運行分析
ANALYZE=true npx webpack --mode production
# 生成 bundle-report.html,在瀏覽器中打開查看
# 常見的體積優化發現:
# 1. moment.js 體積巨大(~300KB),考慮替換為 dayjs(~2KB)
# 2. lodash 按需引入
# import debounce from 'lodash/debounce' 而不是 import _ from 'lodash'
# 3. 檢查是否有重複打包的依賴
完整的生產配置
把上面所有優化整合到一個配置文件中:
javascript
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
mode: 'production',
devtool: 'source-map', // 生產環境使用 source-map 便於錯誤追蹤
entry: {
main: './src/index.tsx',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js',
chunkFilename: 'js/[name].[contenthash:8].chunk.js',
publicPath: '/',
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'],
},
{
test: /\.(png|jpg|gif|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: { maxSize: 8 * 1024 }, // 8KB 以下轉 base64
},
generator: {
filename: 'images/[name].[contenthash:8][ext]',
},
},
],
},
optimization: {
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
compress: { drop_console: true, passes: 2 },
output: { comments: false },
},
}),
new OptimizeCSSAssetsPlugin(),
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 20,
},
},
},
runtimeChunk: 'single',
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html',
minify: {
removeComments: true,
collapseWhitespace: true,
},
}),
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
}),
process.env.ANALYZE && new BundleAnalyzerPlugin(),
].filter(Boolean),
}
小結
- TerserPlugin 是默認的 JS 壓縮器,開啓 parallel 和 drop_console 可以提升壓縮效率
- Tree Shaking 需要 ES Module + 按需導入 + sideEffects 標記三個條件同時滿足
- 代碼分割按 vendors/chunks/共享模塊三層拆分,配合路由懶加載效果最佳
- contenthash 實現持久緩存,runtimeChunk 避免 vendors hash 變化導致緩存失效
- 定期用 Bundle Analyzer 分析體積,大庫考慮按需引入或替換更輕量的替代品
- moment.js (300KB) 替換為 dayjs (2KB)、lodash 全量引入改為按路徑引入,是最常見的體積優化