产物优化
Webpack 通过代码分割、Tree Shaking、压缩与资源模块等减少体积、改善缓存。原理背景见 工作原理。
一、代码分割(Split Chunks)
代码分割的目的是避免重复依赖,将公共代码提取为独立 chunk,充分利用浏览器缓存。
1. SplitChunksPlugin 核心策略
// webpack.config.js
optimization: {
splitChunks: {
chunks: 'all', // 对同步和异步模块都进行分割
minSize: 20000, // 模块大于 20KB 才单独打包(避免过小 chunk)
maxSize: 0, // 不限制最大体积(可设置如 50000)
cacheGroups: { // 分组策略
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10, // 优先级高于默认组
},
common: {
minChunks: 2, // 至少被 2 个 chunk 引用的公共模块
priority: 5,
reuseExistingChunk: true,
},
},
},
}
chunks: 'all':同时分割同步和异步模块,推荐使用。若设为'async'仅分割动态导入模块。minSize:模块体积小于该值的不会单独拆分成 chunk,避免请求过多。maxSize:尝试将大于该值的 chunk 拆分成多个小 chunk,提高缓存命中率(谨慎使用,可能导致请求增多)。cacheGroups:定义提取策略,可以自定义多个分组(如vendor、common、styles等)。
2. 将 node_modules 拆分为单独的 vendor chunk,避免重复依赖
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10,
},
}
- 原理:所有来自
node_modules的模块会被合并到vendors.js中。 - 如果多个入口或异步 chunk 引用了相同的第三方库,此配置可避免重复打包。
3. priority 和 reuseExistingChunk 实战
priority:数值越大,该分组越优先匹配。例如vendor的priority: 10会优先于默认default组的priority: 0,防止公共模块被错误地归入普通 chunk。reuseExistingChunk:如果当前 chunk 包含的模块已在之前的 chunk 中存在,则直接复用,不再重复打包。减少冗余代码。
cacheGroups: {
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
}
4. 将多个异步 chunk 中公共的小模块进一步提取(minChunks 配置)
cacheGroups: {
commons: {
chunks: 'async', // 只对异步 chunk 生效
minChunks: 2, // 至少被 2 个异步 chunk 共用
name: 'common-async',
priority: 5,
reuseExistingChunk: true,
},
}
- 对于动态
import()的页面,如果它们都引用了同一个工具函数(如lodash的某个方法),会被提取到common-async.js,避免重复下载。
二、Tree Shaking
Tree Shaking 删除未使用的代码,前提是必须使用 ES Module(import/export)语法。
1. sideEffects: false 的条件
在 package.json 中标记 "sideEffects": false,告诉 Webpack 该包的模块都没有副作用(即除了导出外没有执行任何代码),可以安全地删除未被引用的导出。
{
"name": "my-library",
"sideEffects": false
}
如果某些文件有副作用(如 import './polyfill.js'),则应列出:
"sideEffects": ["./polyfill.js", "./style.css"]
2. 对 Babel 编译的要求
Babel 默认将 ESM 转换为 CommonJS,这会破坏 Tree Shaking。需要使用 @babel/preset-env 并设置 modules: false 保留 ESM 结构。
// babel.config.js
module.exports = {
presets: [["@babel/preset-env", { modules: false }]],
};
这样 Webpack 才能分析出哪些导出被使用了。
3. 副作用模块如何标记(如 polyfill、CSS 导入)
- 对于必须执行一次的模块(如
import '@babel/polyfill'),不能直接删除,因此需要在package.json的sideEffects中明确指定路径。 - 对于 CSS 文件(
import './style.css'),通常也有副作用(样式插入 DOM),同样需要标记,否则可能会被 Tree Shaking 误删。
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfill.js"
]
4. usedExports 与 Terser 的协作机制
usedExports(Webpack 内置):编译时标记模块中哪些导出被使用了,未被使用的导出会被标记为/* unused */。- Terser:在压缩阶段删除这些未使用的代码(包括函数、变量等)。
optimization: {
usedExports: true, // 开启标记
minimize: true,
}
两者协作流程:
- Webpack 构建模块依赖图,记录每个导出的使用情况。
- 在生成代码时,为未使用的导出添加注释。
- Terser 读取这些注释,安全地删除无用代码。
三、代码压缩
1. TerserWebpackPlugin 详解
Webpack 5 默认内置 Terser 插件,但也可以手动配置增强。
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true, // 多进程并行压缩,提升构建速度
extractComments: false, // 是否将注释提取到单独文件(如 license)
terserOptions: {
compress: {
drop_console: true, // 生产环境移除 console
drop_debugger: true,
},
output: {
comments: false, // 移除所有注释
// keep_fnames: false, // 保留函数名(对调试有用,但会增加体积)
},
},
}),
],
},
};
parallel:利用多核 CPU 加速压缩,建议开启。extractComments:将版权注释提取到.LICENSE.txt,便于符合开源协议。keep_fnames权衡:保留函数名有助于堆栈追踪调试,但会增加体积。对于生产环境一般不开启;如果依赖Function.name做逻辑判断,则需设为true。
2. CSS 压缩:CssMinimizerWebpackPlugin
替代已经废弃的 optimize-css-assets-webpack-plugin。
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
optimization: {
minimizer: [
"...", // 继承默认 JS 压缩(Terser)
new CssMinimizerPlugin({
parallel: true,
minimizerOptions: {
preset: ["default", { discardComments: { removeAll: true } }],
},
}),
],
},
};
注意:CSS 压缩器应放在 minimizer 数组中,与 JS 压缩器并列。
四、资源模块优化
Webpack 5 内置了资源模块(asset modules),取代 file-loader、url-loader、raw-loader。
1. asset 模块类型
| 类型 | 作用 |
|---|---|
asset/resource | 单独输出文件,通过 URL 引用(相当于 file-loader) |
asset/inline | 导出资源的 DataURI(相当于 url-loader 且 limit 无限) |
asset/source | 导出资源的源代码(相当于 raw-loader) |
asset | 自动决定:小于某个阈值时内联为 DataURI,否则输出文件 |
2. 小资源内嵌的阈值设置
module: {
rules: [
{
test: /\.(png|jpg|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024, // 8KB 以下的图片转为 Base64
},
},
},
],
}
- 内联可以减少 HTTP 请求,但会增加 HTML 或 CSS 的体积,需要权衡。
3. 指纹命名([contenthash])的最佳实践
输出文件名使用 [contenthash] 实现长期缓存:
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js',
},
[contenthash]基于模块内容生成,内容不变则哈希不变,利于浏览器缓存。- 对于 CSS 也使用
[contenthash]。
4. 预获取/预加载:/* webpackPrefetch: true */ 与 /* webpackPreload: true */
- 预获取(Prefetch):浏览器空闲时提前下载异步 chunk,用于非关键资源(如下一个页面的 JS)。
import(/* webpackPrefetch: true */ "./LoginModal.vue");
- 预加载(Preload):与当前页面同优先级,立即下载,用于当前页面必需但未立即执行的资源。
import(/* webpackPreload: true */ "./CriticalChart.vue");
原理:Webpack 会在生成的 HTML 中插入 <link rel="prefetch/preload" href="..."> 标签,告诉浏览器提前加载。需要 HtmlWebpackPlugin 配合。
时机:prefetch 适合懒加载页面的资源;preload 适合当前路由下立即需要但未解析的组件。
注意:过度使用 preload 会增加首屏加载负担。
总结
| 优化类别 | 关键技术 |
|---|---|
| 代码分割 | SplitChunksPlugin 配置 chunks: 'all'、cacheGroups、minChunks 等 |
| Tree Shaking | sideEffects: false、Babel 保留 ESM、usedExports + Terser |
| 代码压缩 | TerserWebpackPlugin(JS)、CssMinimizerWebpackPlugin(CSS) |
| 资源优化 | asset 模块、小资源内嵌阈值、[contenthash] 指纹、prefetch/preload |
合理运用这些配置,能显著减少产物体积、优化缓存策略、提升页面加载性能。
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| 代码分割 | 官方 |
| Tree Shaking | 官方 |
| Optimization | 配置项 |
相关文章
工作原理
Webpack 是静态模块打包器:从入口递归解析依赖,经 Loader 转换后输出一个或多个 bundle。与 Vite 对比见 Webpack 与 Vite。
企业级实践
大型前端项目在 Webpack 场景下的多环境拆分、Docker 构建缓存、自定义 CLI 与微前端(Module Federation / single-spa)等实践。入门见 工程化概览。
Loader
Loader 在 Webpack 构建链中把源文件转为可打包的 JS 模块(如 TS、SCSS、Vue SFC)。见 工作原理。
进阶配置
Webpack 进阶场景:多页面(MPA)、devServer、环境变量注入、Source Map、Module Federation 等。基础见 工作原理。
Plugin
Webpack 插件在构建生命周期钩子上扩展能力(修改资源、生成 HTML、上传 CDN 等)。背景见 工作原理;与 Loader 分工:Loader 转换单文件,插件面向整体构建。
其他配置
除 ESLint、Prettier 等规范文件外,仓库中常见的工具链与包管理配置如下。见 工程化概览。