- 发布于
Webpack打包优化实战:代码分割、Tree Shaking与缓存策略
- 作者

- 姓名
- 全能波
- GitHub
- @weicracker
Webpack打包优化实战:代码分割、Tree Shaking与缓存策略
Webpack作为现代前端构建工具的核心,其优化配置直接影响应用性能。本文将分享Webpack打包优化的实战经验和高级技巧。
代码分割策略
基础代码分割配置
// webpack.config.js - 代码分割配置
const path = require('path');
module.exports = {
mode: 'production',
entry: {
main: './src/index.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js',
clean: true,
},
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000,
maxSize: 244000,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
cacheGroups: {
// 第三方库分离
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10,
reuseExistingChunk: true,
},
// React相关库单独分离
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react',
chunks: 'all',
priority: 20,
},
// UI库分离
antd: {
test: /[\\/]node_modules[\\/]antd[\\/]/,
name: 'antd',
chunks: 'all',
priority: 15,
},
// 工具库分离
utils: {
test: /[\\/]node_modules[\\/](lodash|moment|dayjs)[\\/]/,
name: 'utils',
chunks: 'all',
priority: 15,
},
// 公共代码分离
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
priority: 5,
reuseExistingChunk: true,
enforce: true,
},
// 默认分组
default: {
minChunks: 2,
priority: -10,
reuseExistingChunk: true,
},
},
},
// 运行时代码分离
runtimeChunk: {
name: 'runtime',
},
},
};
动态导入与路由分割
// 路由级别的代码分割
import { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
// 懒加载组件
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
// 预加载策略
const DashboardWithPreload = lazy(() =>
import(
/* webpackChunkName: "dashboard" */
/* webpackPreload: true */
'./pages/Dashboard'
)
);
// 按需加载工具函数
const loadComponent = (componentName) => {
return lazy(() =>
import(`./components/${componentName}`)
.catch(() => import('./components/ErrorFallback'))
);
};
function App() {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</Router>
);
}
// 智能预加载Hook
import { useEffect } from 'react';
function usePreloadRoute(routePath, condition = true) {
useEffect(() => {
if (condition) {
// 预加载路由组件
import(
/* webpackChunkName: "[request]" */
`./pages/${routePath}`
).catch(console.error);
}
}, [routePath, condition]);
}
// 使用示例
function HomePage() {
// 当用户在首页时,预加载Dashboard页面
usePreloadRoute('Dashboard', true);
return <div>Home Page</div>;
}
第三方库优化分割
// 第三方库优化配置
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
// 大型UI库单独分割
antd: {
test: /[\\/]node_modules[\\/]antd[\\/]/,
name: 'antd',
chunks: 'all',
priority: 20,
},
// 图表库分割
charts: {
test: /[\\/]node_modules[\\/](echarts|d3|chart\.js)[\\/]/,
name: 'charts',
chunks: 'all',
priority: 15,
},
// 工具库分割
lodash: {
test: /[\\/]node_modules[\\/]lodash[\\/]/,
name: 'lodash',
chunks: 'all',
priority: 15,
},
// 日期库分割
date: {
test: /[\\/]node_modules[\\/](moment|dayjs|date-fns)[\\/]/,
name: 'date-utils',
chunks: 'all',
priority: 15,
},
// 异步加载的第三方库
asyncVendor: {
test: /[\\/]node_modules[\\/]/,
chunks: 'async',
name: 'async-vendor',
priority: 10,
},
},
},
},
// 外部化大型库
externals: {
'react': 'React',
'react-dom': 'ReactDOM',
'lodash': '_',
'moment': 'moment',
},
};
// 对应的HTML模板
/*
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="root"></div>
<!-- CDN引入外部化的库 -->
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/lodash@4/lodash.min.js"></script>
<script src="https://unpkg.com/moment@2/min/moment.min.js"></script>
</body>
</html>
*/
Tree Shaking优化
ES6模块与Tree Shaking
// package.json - 确保sideEffects配置正确
{
"name": "my-app",
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfills.js",
"./src/global-styles.js"
]
}
// webpack.config.js - Tree Shaking配置
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
sideEffects: false,
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
modules: false, // 保持ES6模块格式
useBuiltIns: 'usage',
corejs: 3,
}],
],
},
},
},
],
},
};
// 工具函数模块化设计
// utils/index.js - 避免barrel exports
export { default as formatDate } from './formatDate';
export { default as debounce } from './debounce';
export { default as throttle } from './throttle';
export { default as deepClone } from './deepClone';
// 更好的做法:直接导入具体模块
// import { formatDate } from './utils/formatDate';
// import { debounce } from './utils/debounce';
// utils/formatDate.js
export default function formatDate(date, format = 'YYYY-MM-DD') {
// 实现代码
}
// 避免副作用的模块设计
// ❌ 有副作用的代码
console.log('Module loaded'); // 这会阻止Tree Shaking
// ✅ 无副作用的纯函数
export function pureFunction(input) {
return input * 2;
}
第三方库Tree Shaking
// 针对不同库的Tree Shaking策略
// 1. Lodash优化
// ❌ 导入整个lodash
import _ from 'lodash';
// ✅ 按需导入
import debounce from 'lodash/debounce';
import throttle from 'lodash/throttle';
// 或使用babel-plugin-lodash
// .babelrc
{
"plugins": [
["lodash", { "id": ["lodash"] }]
]
}
// 2. Ant Design优化
// ❌ 导入整个antd
import { Button, Input } from 'antd';
// ✅ 使用babel-plugin-import
// .babelrc
{
"plugins": [
["import", {
"libraryName": "antd",
"libraryDirectory": "es",
"style": "css"
}]
]
}
// 3. 日期库优化
// ❌ 导入整个moment
import moment from 'moment';
// ✅ 使用dayjs或date-fns
import dayjs from 'dayjs';
import { format, parseISO } from 'date-fns';
// 4. 图标库优化
// ❌ 导入所有图标
import * as Icons from '@ant-design/icons';
// ✅ 按需导入图标
import { UserOutlined, SettingOutlined } from '@ant-design/icons';
自定义Tree Shaking插件
// webpack-tree-shaking-plugin.js
class TreeShakingAnalyzerPlugin {
constructor(options = {}) {
this.options = options;
}
apply(compiler) {
compiler.hooks.afterEmit.tap('TreeShakingAnalyzer', (compilation) => {
const stats = compilation.getStats().toJson();
const modules = stats.modules || [];
// 分析未使用的导出
const unusedExports = [];
const sideEffectModules = [];
modules.forEach(module => {
if (module.providedExports && module.usedExports) {
const unused = module.providedExports.filter(
exp => !module.usedExports.includes(exp)
);
if (unused.length > 0) {
unusedExports.push({
module: module.name,
unused: unused
});
}
}
if (module.sideEffectFree === false) {
sideEffectModules.push(module.name);
}
});
// 输出分析报告
if (this.options.verbose) {
console.log('\n=== Tree Shaking 分析报告 ===');
console.log(`未使用的导出: ${unusedExports.length} 个模块`);
console.log(`有副作用的模块: ${sideEffectModules.length} 个`);
if (unusedExports.length > 0) {
console.log('\n未使用的导出:');
unusedExports.forEach(item => {
console.log(` ${item.module}: ${item.unused.join(', ')}`);
});
}
if (sideEffectModules.length > 0) {
console.log('\n有副作用的模块:');
sideEffectModules.forEach(module => {
console.log(` ${module}`);
});
}
}
// 生成报告文件
if (this.options.outputFile) {
const report = {
timestamp: new Date().toISOString(),
unusedExports,
sideEffectModules,
totalModules: modules.length
};
const fs = require('fs');
fs.writeFileSync(
this.options.outputFile,
JSON.stringify(report, null, 2)
);
}
});
}
}
module.exports = TreeShakingAnalyzerPlugin;
// 使用插件
const TreeShakingAnalyzerPlugin = require('./webpack-tree-shaking-plugin');
module.exports = {
plugins: [
new TreeShakingAnalyzerPlugin({
verbose: true,
outputFile: 'tree-shaking-report.json'
})
]
};
缓存策略优化
文件名哈希策略
// webpack.config.js - 缓存优化配置
const path = require('path');
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
// 入口文件使用contenthash
filename: '[name].[contenthash:8].js',
// 异步chunk使用contenthash
chunkFilename: '[name].[contenthash:8].chunk.js',
// 静态资源使用contenthash
assetModuleFilename: 'assets/[name].[contenthash:8][ext]',
},
optimization: {
// 模块ID稳定化
moduleIds: 'deterministic',
chunkIds: 'deterministic',
// 运行时代码分离,避免影响业务代码缓存
runtimeChunk: {
name: 'runtime',
},
splitChunks: {
cacheGroups: {
// 第三方库缓存组
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
// 确保第三方库的稳定缓存
priority: 10,
},
},
},
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
],
},
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024, // 8KB
},
},
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].chunk.css',
}),
],
};
持久化缓存配置
// webpack.config.js - Webpack 5 持久化缓存
module.exports = {
cache: {
type: 'filesystem',
cacheDirectory: path.resolve(__dirname, '.webpack-cache'),
buildDependencies: {
config: [__filename],
tsconfig: [path.resolve(__dirname, 'tsconfig.json')],
},
// 缓存版本控制
version: '1.0.0',
// 缓存压缩
compression: 'gzip',
// 缓存存储策略
store: 'pack',
// 缓存失效条件
managedPaths: [path.resolve(__dirname, 'node_modules')],
profile: true,
},
// 快照配置
snapshot: {
managedPaths: [path.resolve(__dirname, 'node_modules')],
immutablePaths: [],
buildDependencies: {
hash: true,
timestamp: true,
},
module: {
timestamp: true,
},
resolve: {
timestamp: true,
},
resolveBuildDependencies: {
hash: true,
timestamp: true,
},
},
};
// 缓存分析工具
class CacheAnalyzerPlugin {
apply(compiler) {
compiler.hooks.done.tap('CacheAnalyzer', (stats) => {
const compilation = stats.compilation;
const cache = compilation.cache;
if (cache && cache.getCache) {
const cacheStats = {
hits: 0,
misses: 0,
stores: 0,
};
// 分析缓存命中率
console.log('\n=== 缓存分析 ===');
console.log(`缓存命中率: ${((cacheStats.hits / (cacheStats.hits + cacheStats.misses)) * 100).toFixed(2)}%`);
console.log(`缓存命中: ${cacheStats.hits}`);
console.log(`缓存未命中: ${cacheStats.misses}`);
console.log(`缓存存储: ${cacheStats.stores}`);
}
});
}
}
资源预加载与缓存
// 资源预加载策略
class ResourcePreloadPlugin {
constructor(options = {}) {
this.options = {
preload: [],
prefetch: [],
...options
};
}
apply(compiler) {
compiler.hooks.compilation.tap('ResourcePreload', (compilation) => {
compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration.tap(
'ResourcePreload',
(data) => {
const { preload, prefetch } = this.options;
// 添加preload链接
preload.forEach(resource => {
data.assets.js.unshift({
...resource,
attributes: { rel: 'preload', as: resource.as || 'script' }
});
});
// 添加prefetch链接
prefetch.forEach(resource => {
data.assets.js.push({
...resource,
attributes: { rel: 'prefetch' }
});
});
}
);
});
}
}
// Service Worker缓存策略
// sw.js
const CACHE_NAME = 'my-app-v1.0.0';
const STATIC_CACHE = 'static-v1.0.0';
const DYNAMIC_CACHE = 'dynamic-v1.0.0';
// 静态资源缓存策略
const STATIC_ASSETS = [
'/',
'/static/js/runtime.js',
'/static/js/vendors.js',
'/static/css/main.css',
];
// 缓存策略配置
const CACHE_STRATEGIES = {
// 静态资源:缓存优先
static: {
pattern: /\.(js|css|png|jpg|jpeg|gif|svg|woff|woff2)$/,
strategy: 'cacheFirst',
maxAge: 30 * 24 * 60 * 60 * 1000, // 30天
},
// API请求:网络优先
api: {
pattern: /\/api\//,
strategy: 'networkFirst',
maxAge: 5 * 60 * 1000, // 5分钟
},
// HTML页面:网络优先,离线回退
pages: {
pattern: /\.html$/,
strategy: 'networkFirst',
fallback: '/offline.html',
},
};
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(STATIC_CACHE)
.then(cache => cache.addAll(STATIC_ASSETS))
);
});
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// 选择缓存策略
const strategy = Object.values(CACHE_STRATEGIES)
.find(s => s.pattern.test(url.pathname));
if (strategy) {
event.respondWith(handleRequest(request, strategy));
}
});
async function handleRequest(request, strategy) {
const cache = await caches.open(DYNAMIC_CACHE);
switch (strategy.strategy) {
case 'cacheFirst':
return cacheFirst(request, cache, strategy);
case 'networkFirst':
return networkFirst(request, cache, strategy);
default:
return fetch(request);
}
}
async function cacheFirst(request, cache, strategy) {
const cached = await cache.match(request);
if (cached) {
return cached;
}
try {
const response = await fetch(request);
if (response.ok) {
cache.put(request, response.clone());
}
return response;
} catch (error) {
return new Response('Network error', { status: 408 });
}
}
async function networkFirst(request, cache, strategy) {
try {
const response = await fetch(request);
if (response.ok) {
cache.put(request, response.clone());
}
return response;
} catch (error) {
const cached = await cache.match(request);
return cached || new Response('Offline', { status: 503 });
}
}
构建性能优化
并行处理与缓存
// webpack.config.js - 构建性能优化
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
// 并行压缩JS
new TerserPlugin({
parallel: true,
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
}),
// 并行压缩CSS
new CssMinimizerPlugin({
parallel: true,
}),
],
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
// 并行处理
{
loader: 'thread-loader',
options: {
workers: require('os').cpus().length - 1,
},
},
{
loader: 'babel-loader',
options: {
cacheDirectory: true,
cacheCompression: false,
},
},
],
},
],
},
// 解析优化
resolve: {
modules: [
path.resolve(__dirname, 'src'),
'node_modules',
],
extensions: ['.js', '.jsx', '.ts', '.tsx'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
// 缓存解析结果
cache: true,
},
// 监听优化
watchOptions: {
ignored: /node_modules/,
aggregateTimeout: 300,
poll: 1000,
},
};
总结
Webpack优化的核心要点:
- 代码分割:合理的chunk分割策略,提升缓存效率
- Tree Shaking:消除死代码,减小bundle体积
- 缓存策略:文件名哈希、持久化缓存、Service Worker
- 构建性能:并行处理、缓存机制、解析优化
- 监控分析:构建分析工具,持续优化
Webpack优化是一个持续的过程,需要根据项目特点和性能需求不断调整和完善配置。