代码分割(Code Splitting)
概念
代码分割是将 bundle 拆分成多个小块,按需加载的技术。
┌──────────────────────────────────────────────────────────────┐
│ 代码分割示意图 │
├──────────────────────────────────────────────────────────────┤
│ │
│ 分割前 │
│ ┌─────────────────────────┐ │
│ │ main.bundle.js │ │
│ │ (包含所有代码,体积大) │ │
│ └─────────────────────────┘ │
│ │
│ 分割后 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ main.js │ │ vendor.js │ │ lazy.js │ │
│ │ (主代码) │ │ (第三方库) │ │ (按需加载) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
入口分割
// webpack.config.js
module.exports = {
entry: {
main: './src/index.js',
vendor: './src/vendor.js',
analytics: './src/analytics.js' // 独立打包
},
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js'
}
};
动态导入(推荐)
// 使用 import() 自动代码分割
const getComponent = () => {
return import('./Component')
.then(module => {
const Component = module.default;
return new Component();
});
};
// React 中使用 React.lazy
const LazyComponent = React.lazy(() => import('./HeavyComponent'));
// Vue 中使用
const HeavyComponent = () => import('./HeavyComponent.vue');
// 使用命名导入
const module = await import('./utils.js');
const { helper, format } = await import('./utils.js');
配置 optimization.splitChunks
module.exports = {
optimization: {
splitChunks: {
// 'all' | 'async' | 'initial'
chunks: 'all',
// 最小块大小(字节)
minSize: 20000,
// 最大块大小
maxSize: 244000,
// 最小块被引用次数
minChunks: 1,
// 最大请求数(并行加载)
maxAsyncRequests: 30,
// 最大入口数
maxInitialRequests: 30,
// 自动生成名字的分隔符
automaticNameDelimiter: '~',
// 缓存组
cacheGroups: {
// 默认组
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
},
// vendors 组
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10
},
// common 组
common: {
name: 'common',
minChunks: 2,
chunks: 'initial',
priority: 5,
reuseExistingChunk: true
},
// 按需加载的组
async: {
test: /[\\/]src[\\/]views[\\/]/,
name: 'async-views',
chunks: 'async',
priority: 10
}
}
},
// 运行时 chunk
runtimeChunk: 'single',
// 模块 ID
moduleIds: 'deterministic'
}
};
Tree Shaking
概念与原理
Tree Shaking 是通过静态分析移除未使用的代码(dead code)。
// webpack.config.js
module.exports = {
mode: 'production', // 生产模式自动启用
// 或者手动配置
optimization: {
usedExports: true, // 标记未使用的导出
sideEffects: true // 启用副作用优化
}
};
sideEffects 配置
// package.json
{
"sideEffects": [
"*.css",
"*.scss"
]
}
/* button.css - 被标记为有副作用,不能被删除 */
.button {
background: blue;
}
// util.js - 没有副作用,未使用的可以删除
export const add = (a, b) => a + b;
export const multiply = (a, b) => a * b;
// 如果只用了 add,multiply 会被删除
import { add } from './util.js';
手动标记
// webpack.config.js
module.exports = {
optimization: {
sideEffects: {
// 可以在此处指定哪些文件有副作用
// 默认会读取 package.json 的 sideEffects
}
}
};
CSS Tree Shaking
// 安装 purgecss-webpack-plugin
const PurgeCSSPlugin = require('purgecss-webpack-plugin');
const glob = require('glob');
const path = require('path');
module.exports = {
plugins: [
new PurgeCSSPlugin({
paths: glob.sync(path.join(__dirname, 'src/**/*.js')),
// 白名单
whitelist: ['ignored-class', 'hover-state']
})
]
};
运行时优化
持久化缓存
module.exports = {
cache: {
type: 'filesystem', // 使用文件系统缓存
buildDependencies: {
config: [__filename] // 配置变化时清除缓存
},
compression: 'gzip' // 压缩缓存
}
};
并行构建
// 使用 thread-loader
module.exports = {
module: {
rules: [
{
test: /\.jsx?$/,
use: [
{
loader: 'thread-loader',
options: {
workers: require('os').cpus().length - 1,
poolTimeout: Infinity // 保持 worker
}
},
'babel-loader'
]
}
]
}
};
// 或者使用 esbuild-loader
module.exports = {
module: {
rules: [
{
test: /\.jsx?$/,
loader: 'esbuild-loader',
options: {
target: 'es2015'
}
}
]
}
};
输出控制
module.exports = {
output: {
// 清理输出目录
clean: true, // 或 cleanOnceBeforeBuildPatterns
// 资源模块(Asset Modules)
assetModuleFilename: 'assets/[hash:8][ext][query]',
// 唯一哈希
hashFunction: 'xxhash64'
}
};
Module Federation(微前端)
概念
Module Federation 允许独立构建的 Webpack bundles 互相共享模块。
┌──────────────────────────────────────────────────────────────┐
│ Module Federation 架构 │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Host App │ │ Remote │ │
│ │ (主应用) │ ───────→│ App │ │
│ │ │ 共享模块 │ (远程) │ │
│ └─────────────┘ └─────────────┘ │
│ │ │ │
│ │ │ │
│ └─────────────┬───────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Shared │ │
│ │ Dependencies │ │
│ │ (React, Redux) │ │
│ └─────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
Host(主机)配置
// webpack.host.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const deps = require('./package.json').dependencies;
module.exports = {
mode: 'production',
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
// 定义远程应用
remoteApp: 'remote@http://localhost:3001/remoteEntry.js'
},
shared: {
// 共享的依赖(单例模式)
react: { singleton: true, requiredVersion: deps.react },
'react-dom': { singleton: true, requiredVersion: deps['react-dom'] },
redux: { singleton: true }
}
})
]
};
Remote(远程)配置
// webpack.remote.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const deps = require('./package.json').dependencies;
module.exports = {
mode: 'production',
plugins: [
new ModuleFederationPlugin({
name: 'remote',
filename: 'remoteEntry.js', // 远程入口文件名
// 暴露的模块
exposes: {
'./Button': './src/components/Button',
'./Modal': './src/components/Modal'
},
shared: {
react: { singleton: true, requiredVersion: deps.react },
'react-dom': { singleton: true, requiredVersion: deps['react-dom'] }
}
})
]
};
在 Host 中使用 Remote
// App.jsx
import React, { Suspense } from 'react';
// 动态导入远程模块
const RemoteButton = React.lazy(() => import('remote/Button'));
function App() {
return (
<div>
<h1>Host Application</h1>
<Suspense fallback={<div>Loading Button...</div>}>
<RemoteButton />
</Suspense>
</div>
);
}
全局变量方式(不推荐但简单)
// Remote 端
new ModuleFederationPlugin({
name: 'remote',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button'
}
});
// Host 端在 HTML 中引入
// <script src="http://localhost:3001/remoteEntry.js"></script>
// 然后直接使用
const RemoteButton = window.remote.Button;
性能优化进阶
分析工具
// webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-report.html',
openAnalyzer: false
})
]
};
预取/预加载
// 在代码中添加注释引导 Webpack
import(/* webpackPrefetch: true */ './Component');
import(/* webpackPreload: true */ './EarlyComponent');
外部扩展(Externals)
module.exports = {
externals: {
// 外部 CDN
react: 'React',
'react-dom': 'ReactDOM',
// 外部文件
lodash: '_'
}
};
// CDN 引入
// <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
DLL Plugin(已过时,但了解历史)
// webpack.dll.config.js
const webpack = require('webpack');
module.exports = {
mode: 'production',
entry: {
vendor: ['react', 'react-dom', 'lodash']
},
output: {
path: path.resolve(__dirname, 'dll'),
filename: '[name].dll.js',
library: '[name]'
},
plugins: [
new webpack.DllPlugin({
name: '[name]',
path: path.resolve(__dirname, 'dll/[name].manifest.json')
})
]
};
配置示例:完整生产配置
// webpack.prod.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
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',
assetModuleFilename: 'assets/[hash:8][ext][query]',
clean: true,
publicPath: '/'
},
// 模块解析
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components')
}
},
// 模块规则
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader'
]
},
{
test: /\.(png|jpe?g|gif|svg|webp)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024
}
}
}
]
},
// 插件
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
}
}),
new MiniCssExtractPlugin({
filename: '[name].[contenthash:8].css',
chunkFilename: '[name].[contenthash:8].chunk.css'
}),
// 仅在分析时启用
// new BundleAnalyzerPlugin()
],
// 优化
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
}),
new CssMinimizerPlugin()
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
},
runtimeChunk: 'single',
moduleIds: 'deterministic'
},
// 性能
performance: {
hints: 'warning',
maxEntrypointSize: 512000,
maxAssetSize: 512000
},
// 统计信息
stats: {
assets: true,
chunks: false,
modules: false
}
};
这一章想说的
Webpack 高级配置:
- 代码分割:入口分割、动态 import、splitChunks 配置
- Tree Shaking:移除未使用代码,需要正确配置 sideEffects
- 运行时优化:持久化缓存、并行构建、输出控制
- Module Federation:微前端架构的核心,让多个独立应用共享模块
- 性能分析:使用 bundle-analyzer 分析包体积
掌握这些高级特性才能构建高效的前端工程。