Webpack 优化细节详述
本地开发优化
开启模块热替换(HMR)使用 Source Maps 优化编译速度
模块热替换(HMR)配置
HMR 允许在运行时更新模块,无需完整刷新页面,大大提升开发效率。
javascript
// webpack.dev.js
const webpack = require('webpack');
module.exports = {
mode: 'development',
devServer: {
hot: true, // 启用 HMR
port: 3000,
open: true,
compress: true,
historyApiFallback: true,
static: {
directory: path.join(__dirname, 'public'),
},
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
],
// 快速的 source map,适合开发环境
devtool: 'eval-cheap-module-source-map',
};
Source Maps 最佳配置
不同的 source map 类型对编译速度影响很大:
javascript
// 开发环境 source map 配置选择
const sourceMapConfig = {
// 最快,但质量较低
fastest: 'eval',
// 平衡速度和质量
balanced: 'eval-cheap-module-source-map',
// 高质量,但较慢
quality: 'eval-source-map',
// 生产环境
production: 'source-map'
};
module.exports = {
devtool: process.env.NODE_ENV === 'production'
? sourceMapConfig.production
: sourceMapConfig.balanced,
};
HMR 工作流程
graph TD
A[文件变更] --> B[Webpack 检测变化]
B --> C[重新编译变更模块]
C --> D[生成 Hot Update]
D --> E[推送到浏览器]
E --> F[HMR Runtime 接收]
F --> G{模块支持 HMR?}
G -->|是| H[模块热更新]
G -->|否| I[页面刷新]
H --> J[UI 更新完成]
I --> J
使用持久化缓存
Webpack 5 引入的持久化缓存功能可以显著提升构建速度:
javascript
// webpack.config.js
module.exports = {
cache: {
type: 'filesystem', // 使用文件系统缓存
buildDependencies: {
config: [__filename], // 当配置文件改变时,缓存失效
},
cacheDirectory: path.resolve(__dirname, '.webpack-cache'),
store: 'pack', // 缓存存储方式
version: '1.0', // 缓存版本
},
// 优化缓存命中率
optimization: {
moduleIds: 'deterministic',
chunkIds: 'deterministic',
},
};
缓存配置详解
javascript
// 高级缓存配置
const cacheConfig = {
cache: {
type: 'filesystem',
// 缓存版本管理
version: createHash('md5')
.update(JSON.stringify({
nodeVersion: process.version,
webpackVersion: require('webpack/package.json').version,
configHash: getConfigHash(),
}))
.digest('hex'),
// 缓存依赖项
buildDependencies: {
config: [
__filename,
path.resolve(__dirname, 'babel.config.js'),
path.resolve(__dirname, 'postcss.config.js'),
],
},
// 自定义缓存目录
cacheDirectory: path.resolve(__dirname, 'node_modules/.cache/webpack'),
// 缓存策略
managedPaths: [
path.resolve(__dirname, 'node_modules'),
],
// 缓存压缩
compression: 'gzip',
},
};
更快的增量编译
优化模块解析
javascript
module.exports = {
resolve: {
// 减少文件扩展名解析
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
// 优化模块查找路径
modules: [
path.resolve(__dirname, 'src'),
'node_modules',
],
// 设置别名减少查找时间
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
},
// 优化解析器缓存
unsafeCache: true,
// 符合链接优化
symlinks: false,
},
// 优化模块构建
module: {
// 明确不解析的模块
noParse: /jquery|lodash/,
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
include: path.resolve(__dirname, 'src'),
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true, // 启用 Babel 缓存
cacheCompression: false,
},
},
},
],
},
};
多进程构建
javascript
const TerserPlugin = require('terser-webpack-plugin');
const HappyPack = require('happypack');
const os = require('os');
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: 'happypack/loader?id=babel',
exclude: /node_modules/,
},
],
},
plugins: [
new HappyPack({
id: 'babel',
threads: os.cpus().length,
loaders: ['babel-loader?cacheDirectory=true'],
}),
],
optimization: {
minimizer: [
new TerserPlugin({
parallel: true, // 启用多进程压缩
terserOptions: {
compress: {
drop_console: true,
},
},
}),
],
},
};
线上产物构建优化
使用生产模式
javascript
// webpack.prod.js
module.exports = {
mode: 'production', // 自动启用多项优化
// 生产环境优化配置
optimization: {
minimize: true,
sideEffects: false, // 启用 Tree Shaking
usedExports: true,
// 代码分割配置
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
},
common: {
name: 'common',
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
},
},
},
// 运行时代码单独打包
runtimeChunk: {
name: 'runtime',
},
},
// 生产环境 source map
devtool: 'source-map',
};
代码分割
动态导入实现代码分割
javascript
// 路由级别的代码分割
const routes = [
{
path: '/home',
component: () => import(/* webpackChunkName: "home" */ '@/views/Home.vue'),
},
{
path: '/about',
component: () => import(/* webpackChunkName: "about" */ '@/views/About.vue'),
},
];
// 组件级别的代码分割
const LazyComponent = lazy(() =>
import(/* webpackChunkName: "lazy-component" */ './LazyComponent')
);
// 工具库按需加载
async function loadUtility() {
const { debounce } = await import(
/* webpackChunkName: "lodash-debounce" */
'lodash/debounce'
);
return debounce;
}
高级代码分割配置
javascript
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000,
maxSize: 250000,
cacheGroups: {
// 第三方库分割
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
chunks: 'all',
},
// React 相关库单独分割
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react',
priority: 20,
chunks: 'all',
},
// 工具库分割
utils: {
test: /[\\/]node_modules[\\/](lodash|moment|axios)[\\/]/,
name: 'utils',
priority: 15,
chunks: 'all',
},
// 公共代码分割
common: {
name: 'common',
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
},
},
},
},
};
代码分割流程
graph TD
A[应用入口] --> B[分析依赖图]
B --> C[识别分割点]
C --> D[动态导入]
C --> E[路由分割]
C --> F[第三方库分割]
D --> G[生成 Chunk]
E --> G
F --> G
G --> H[文件命名]
H --> I[输出文件]
I --> J{需要时加载}
J -->|是| K[异步加载 Chunk]
J -->|否| L[保持空闲]
压缩 JavaScript
TerserPlugin 配置
javascript
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
parallel: true, // 多进程压缩
extractComments: false, // 不提取注释到单独文件
terserOptions: {
compress: {
drop_console: true, // 移除 console
drop_debugger: true, // 移除 debugger
pure_funcs: ['console.log'], // 移除指定函数调用
unused: true, // 移除未使用的代码
},
mangle: {
safari10: true, // 兼容 Safari 10
},
format: {
comments: false, // 移除注释
},
},
}),
],
},
};
高级 JavaScript 压缩
javascript
// 自定义压缩配置
const compressionConfig = {
// 生产环境压缩配置
production: {
compress: {
arguments: false,
dead_code: true,
drop_console: true,
drop_debugger: true,
evaluate: true,
hoist_funs: true,
hoist_vars: false,
if_return: true,
join_vars: true,
keep_fargs: false,
loops: true,
passes: 2, // 多次压缩
pure_funcs: [
'console.log',
'console.info',
'console.debug',
'console.warn',
],
reduce_vars: true,
sequences: true,
side_effects: false,
typeofs: false,
unused: true,
},
mangle: {
properties: {
regex: /^_/, // 混淆以 _ 开头的属性
},
},
},
};
压缩 CSS
CSS 压缩配置
javascript
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
['autoprefixer'],
['cssnano', { preset: 'default' }],
],
},
},
},
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
chunkFilename: '[id].[contenthash].css',
}),
],
optimization: {
minimizer: [
new CssMinimizerPlugin({
parallel: true,
minimizerOptions: {
preset: [
'default',
{
discardComments: { removeAll: true },
normalizeWhitespace: true,
colormin: true,
convertValues: true,
discardDuplicates: true,
discardEmpty: true,
mergeRules: true,
minifyFontValues: true,
minifySelectors: true,
},
],
},
}),
],
},
};
启用 Tree Shaking 持久化缓存
Tree Shaking 配置
javascript
module.exports = {
mode: 'production',
optimization: {
usedExports: true, // 标记使用的导出
sideEffects: false, // 标记无副作用
},
// 在 package.json 中配置
// {
// "sideEffects": [
// "*.css",
// "*.scss",
// "./src/polyfills.js"
// ]
// }
};
ES6 模块最佳实践
javascript
// 推荐:命名导出,支持 Tree Shaking
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
// 避免:默认导出整个对象
// export default { add, subtract, multiply };
// 使用时只导入需要的函数
import { add, subtract } from './math-utils';
// Lodash Tree Shaking 示例
import debounce from 'lodash/debounce';
import throttle from 'lodash/throttle';
// 而不是:import { debounce, throttle } from 'lodash';
Bundle 分析
Bundle 分析工具配置
javascript
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: process.env.ANALYZE ? 'server' : 'disabled',
analyzerHost: '127.0.0.1',
analyzerPort: 8888,
reportFilename: 'bundle-report.html',
defaultSizes: 'parsed',
openAnalyzer: true,
generateStatsFile: true,
statsFilename: 'bundle-stats.json',
}),
],
};
// package.json scripts
// {
// "analyze": "ANALYZE=true npm run build",
// "build:stats": "webpack --profile --json > stats.json"
// }
性能监控配置
javascript
module.exports = {
performance: {
hints: 'warning',
maxEntrypointSize: 250000, // 入口文件大小限制
maxAssetSize: 200000, // 单个资源大小限制
assetFilter: function(assetFilename) {
// 只监控 JS 和 CSS 文件
return assetFilename.endsWith('.js') || assetFilename.endsWith('.css');
},
},
// 输出构建信息
stats: {
assets: true,
chunks: true,
modules: false,
reasons: false,
usedExports: true,
providedExports: true,
},
};
懒加载和按需加载压缩图片
图片优化配置
javascript
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
module.exports = {
module: {
rules: [
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024, // 8KB 以下转为 base64
},
},
generator: {
filename: 'images/[name].[hash:8][ext]',
},
},
],
},
plugins: [
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.imageminMinify,
options: {
plugins: [
['imagemin-pngquant', { quality: [0.6, 0.8] }],
['imagemin-mozjpeg', { quality: 80 }],
['imagemin-svgo', {
plugins: [
{ name: 'removeViewBox', active: false },
{ name: 'removeDimensions', active: true },
],
}],
],
},
},
generator: [
{
type: 'asset',
preset: 'webp-custom-name',
implementation: ImageMinimizerPlugin.imageminGenerate,
options: {
plugins: ['imagemin-webp'],
},
},
],
}),
],
};
响应式图片加载
javascript
// 图片懒加载组件
import { useState, useEffect, useRef } from 'react';
const LazyImage = ({ src, alt, className, placeholder }) => {
const [imageSrc, setImageSrc] = useState(placeholder);
const [imageRef, setImageRef] = useState();
useEffect(() => {
let observer;
if (imageRef && imageSrc === placeholder) {
observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setImageSrc(src);
observer.unobserve(imageRef);
}
});
},
{ threshold: 0.1 }
);
observer.observe(imageRef);
}
return () => {
if (observer && observer.unobserve) {
observer.unobserve(imageRef);
}
};
}, [imageRef, imageSrc, placeholder, src]);
return (
<img
ref={setImageRef}
src={imageSrc}
alt={alt}
className={className}
loading="lazy"
/>
);
};
// WebP 支持检测
const supportsWebP = () => {
return new Promise(resolve => {
const webP = new Image();
webP.onload = webP.onerror = () => {
resolve(webP.height === 2);
};
webP.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA';
});
};
内联 CSS 和 JavaScript
关键资源内联
javascript
const HtmlWebpackPlugin = require('html-webpack-plugin');
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: 'public/index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}),
// 内联运行时代码
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
],
optimization: {
runtimeChunk: {
name: 'runtime',
},
},
};
关键 CSS 内联
javascript
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlCriticalWebpackPlugin = require('html-critical-webpack-plugin');
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
}),
new HtmlCriticalWebpackPlugin({
base: path.resolve(__dirname, 'dist'),
src: 'index.html',
dest: 'index.html',
inline: true,
minify: true,
extract: true,
width: 375,
height: 565,
penthouse: {
blockJSRequests: false,
},
}),
],
};
完整示例配置
开发环境配置
javascript
// webpack.dev.js
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
main: './src/index.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
chunkFilename: '[name].chunk.js',
publicPath: '/',
},
devtool: 'eval-cheap-module-source-map',
devServer: {
hot: true,
port: 3000,
open: true,
compress: true,
historyApiFallback: true,
static: {
directory: path.join(__dirname, 'public'),
},
},
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename],
},
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
},
modules: [
path.resolve(__dirname, 'src'),
'node_modules',
],
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
presets: ['@babel/preset-env', '@babel/preset-react'],
plugins: ['@babel/plugin-syntax-dynamic-import'],
},
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader'],
},
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024,
},
},
},
],
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: 'public/index.html',
inject: true,
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development'),
}),
],
optimization: {
moduleIds: 'named',
chunkIds: 'named',
},
};
生产环境配置
javascript
// webpack.prod.js
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
mode: 'production',
entry: {
main: './src/index.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js',
publicPath: '/',
clean: true,
},
devtool: 'source-map',
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename],
},
version: '1.0',
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
},
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
presets: [
['@babel/preset-env', {
modules: false,
useBuiltIns: 'usage',
corejs: 3,
}],
'@babel/preset-react',
],
plugins: ['@babel/plugin-syntax-dynamic-import'],
},
},
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
],
},
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024,
},
},
generator: {
filename: 'images/[name].[hash:8][ext]',
},
},
],
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: 'public/index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}),
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
chunkFilename: '[id].[contenthash].css',
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),
}),
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8,
}),
process.env.ANALYZE && new BundleAnalyzerPlugin(),
].filter(Boolean),
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true,
extractComments: false,
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log'],
},
mangle: {
safari10: true,
},
format: {
comments: false,
},
},
}),
new CssMinimizerPlugin({
parallel: true,
}),
],
moduleIds: 'deterministic',
chunkIds: 'deterministic',
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
},
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react',
priority: 20,
},
common: {
name: 'common',
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
},
},
},
runtimeChunk: {
name: 'runtime',
},
usedExports: true,
sideEffects: false,
},
performance: {
hints: 'warning',
maxEntrypointSize: 250000,
maxAssetSize: 200000,
},
};
构建优化流程总览
graph TD
A[开始构建] --> B{环境判断}
B -->|开发环境| C[启用 HMR]
B -->|生产环境| D[启用压缩]
C --> E[快速 Source Map]
C --> F[持久化缓存]
D --> G[代码分割]
D --> H[Tree Shaking]
D --> I[资源压缩]
E --> J[增量编译]
F --> J
G --> K[Bundle 分析]
H --> K
I --> K
J --> L[开发服务器]
K --> M[生产构建]
L --> N[热更新]
M --> O[部署优化]
N --> P[开发完成]
O --> Q[上线部署]
通过以上配置和优化策略,可以显著提升 Webpack 项目的构建性能和运行效率。开发环境注重快速编译和调试体验,生产环境则专注于代码质量和加载性能的优化。根据项目实际需求,可以灵活调整相关配置参数。