代码拆分
约 2249 字大约 8 分钟
2025-03-15
Webpack 的 代码拆分(Code Splitting) 是一种将代码分割成多个 bundle 或 chunk 的技术,目的是减少初始加载时间,优化应用性能。
Webpack 打包代码时会根据 chunk 来将所有 js 文件打包到一个文件中,这样打包出来的文件可能体积很大。所以,我们需要基于一定的策略和维度来将打包生成的文件进行代码分割,生成多个 js 文件,渲染哪个页面就只加载某个 js 文件,这样加载的资源就少,速度就更快。
核心目标
- 减少首屏加载时间:拆分关键代码与非关键代码。
- 按需加载:只在需要时加载特定的组件、路由或者功能模块,减少初始加载时间。
- 并行加载:利用浏览器多线程下载多个小文件。
- 将代码拆分成多个文件:将应用代码分割成多个 bundle 或 chunk,而不是打包成一个巨大的文件。
- 提高缓存利用率:通过拆分,尽量减小代码的影响范围,利用浏览器缓存提升性能。
场景策略选择
场景 | 推荐策略 |
---|---|
多页面应用(MPA) | 多入口 + SplitChunksPlugin |
单页面应用(SPA)路由 | 动态导入 + 魔法注释 |
第三方库优化 | SplitChunksPlugin 提取 node_modules |
公共工具函数 | 手动分离为独立入口或 cacheGroups |
非首屏组件(如弹窗) | 动态导入 + webpackPrefetch |
拆分策略
1、设置 splitChunks.chunks
为 all
我们可以选择将 splitChunks.chunks
设置为 all
,这样 Webpack 会对所有的 chunk 进行代码分割,包括异步和同步的 chunk。
通俗来讲,只要是 import
出现的地方,都会被代码分割。
module.exports = {
optimization: {
splitChunks: {
chunks: "all", // 对所有 chunks 优化(包括异步和同步)
},
},
};
2、多入口拆分(Entry Points)
通过配置多个入口文件,手动分离代码。适用于多页面应用(MPA)或明确需要分离的独立模块。
Webpack 会根据配置的入口文件,生成多个独立的 chunk。每个 chunk 对应一个入口文件以及其依赖的模块。
// webpack.config.js
module.exports = {
entry: {
app: "./src/app.js",
vendor: ["react", "react-dom"], // 第三方依赖单独打包
},
};
3、拆分依赖包 node_modules
依赖包的拆分,我们采用 手动拆分 + 自动拆分 的方式。
- 手动拆分:我们需要手动识别
node_modules
中较大的包,如lodash
、moment
。将其提取出来,单独打包。 - 自动拆分:其他的包,默认让 Webpack 自动拆分。可以设置阈值,如:低于 100KB 的包不拆分,大于 500KB 的包尝试进一步拆分。
module.exports = {
optimization: {
splitChunks: {
chunks: "all", // 对所有 chunks 优化(包括异步和同步)
cacheGroups: {
lodash: {
test: /[\\/]node_modules[\\/]lodash[\\/]/, // 手动拆分 lodash
name: "lodash",
priority: 30,
},
moment: {
test: /[\\/]node_modules[\\/]moment[\\/]/, // 手动拆分 moment
name: "moment",
priority: 20,
},
// 其余依赖包
vendors: {
test: /[\\/]node_modules[\\/]/, // 拆分 node_modules
name: (module) => {
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/);
return `npm.${packageName.replace('@', '')}`; // 按包名独立切割(如 npm.lodash)
}
minSize: 100000, // 100KB 以上才拆分
maxSize: 500000, // 超过 500KB 尝试进一步拆分
priority: 10, // 优先级最低
},
},
},
},
};
4、拆分公共代码、业务组件
我们可以将代码中 不怎么变化的代码,如 layout
布局组件、util
工具函数、component
组件等,提取出来,单独打包。
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
// 1. 拆分 layout 组件
layout: {
test: /[\\/]src[\\/]layout[\\/]/,
name: "layout",
priority: 30,
minSize: 0, // 即使很小也强制拆分
reuseExistingChunk: true,
},
// 2. 拆分 utils 工具函数
utils: {
test: /[\\/]src[\\/]utils[\\/]/,
name: "utils",
priority: 20,
enforce: true, // 忽略minSize等限制强制拆分
},
// 3. 拆分 component 业务组件
component: {
test: /[\\/]src[\\/]component[\\/]/,
name: "component",
priority: 20,
reuseExistingChunk: true,
},
// 4. 拆分 common 公共代码
common: {
minChunks: 2, // 被至少 2 个 chunk 引用的模块
name: "common",
minSize: 100000, // 100KB 以上才拆分
priority: 5,
},
},
},
},
};
5、魔法注释(Magic Comments)
Webpack 提供了魔法注释 /* webpackChunkName: "Name" */
,用于精准控制动态导入 import()
的行为和生成 chunk 的名称。
魔法注释指定的切割名称,会体现到 output 的输出名上。同名的魔法注释,会合并到一个 chunk 文件中。
对应输出
假如:魔法注释为 /* webpackChunkName: "layout" */
,编译输出规则为 [name].[contenthash].js
,则可能输出 layout.123456.js
// 动态导入生成 Async Chunk
const lazyModule = () => import(/* webpackChunkName: "lazy" */ "./lazy.js");
// 路由懒加载 (React)
const Home = React.lazy(() =>
import(/* webpackChunkName: "home" */ "./pages/Home")
);
// 以下两个导入会合并到同一个 vendors-lodash.js 文件
import(/* webpackChunkName: "vendors-lodash" */ "lodash/get");
import(/* webpackChunkName: "vendors-lodash" */ "lodash/set");
6、CSS 代码提取
通过 mini-css-extract-plugin
分离 CSS 文件。
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
plugins: [new MiniCssExtractPlugin({ filename: "[name].[contenthash].css" })],
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
};
输出效果:
- 生成独立的
main.css
文件,避免 CSS 内联到 JS 中。
7、拆分 runtime 文件
在 Webpack 中,runtime 代码是指用于管理模块加载和缓存的引导代码。合理拆分 runtime 可以优化长期缓存策略,避免因微小改动导致整个 bundle 缓存失效。
module.exports = {
optimization: {
runtimeChunk: "single", // 所有入口共享同一个runtime文件
// 按需拆分
runtimeChunk: {
name: (entrypoint) => `runtime-${entrypoint.name}`, // 每个入口独立的runtime
},
// 或者配合 splitChunks 一起使用
runtimeChunk: "single",
splitChunks: {
cacheGroups: {
runtime: {
name: "runtime",
test: /runtime/,
chunks: "all",
enforce: true,
},
},
},
},
};
完整配置
以下是一个完整的配置文件,需要注意代码拆分策略 splitChunks
的优先级
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
entry: {
home: "./home.js",
about: "./about.js",
contact: "./contact.js",
vendor: ["react", "react-dom"], // 第三方依赖单独打包
},
plugins: [
new MiniCssExtractPlugin({
filename: "static/css/[name].[contenthash:10].css",
chunkFilename: "static/css/[name].[contenthash:10].chunk.css",
}),
],
optimization: {
// 提取 runtime 文件
runtimeChunk: {
name: (entrypoint) => `runtime-${entrypoint.name}`, // 每个入口独立的runtime
},
// 代码分割
splitChunks: {
chunks: "all", // 对所有 chunks 优化(包括异步和同步)
cacheGroups: {
lodash: {
test: /[\\/]node_modules[\\/]lodash[\\/]/, // 手动拆分 lodash
name: "lodash",
priority: 50,
},
moment: {
test: /[\\/]node_modules[\\/]moment[\\/]/, // 手动拆分 moment
name: "moment",
priority: 50,
},
// 其余依赖包
vendors: {
test: /[\\/]node_modules[\\/]/, // 拆分 node_modules
name: (module) => {
const packageName = module.context.match(
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
);
return `npm.${packageName.replace("@", "")}`; // 按包名独立切割(如 npm.lodash)
},
minSize: 100000, // 100KB 以上才拆分
maxSize: 500000, // 超过 500KB 尝试进一步拆分
priority: 40, // 在拆分 Vendor 的策略里优先级最低
},
// 1. 拆分 layout 组件
layout: {
test: /[\\/]src[\\/]layout[\\/]/,
name: "layout",
priority: 30,
minSize: 0, // 即使很小也强制拆分
reuseExistingChunk: true,
},
// 2. 拆分 utils 工具函数
utils: {
test: /[\\/]src[\\/]utils[\\/]/,
name: "utils",
priority: 30,
enforce: true, // 忽略minSize等限制强制拆分
},
// 3. 拆分 component 业务组件
component: {
test: /[\\/]src[\\/]component[\\/]/,
name: "component",
priority: 30,
reuseExistingChunk: true,
},
// 4. 拆分 common 公共代码
common: {
minChunks: 2, // 被至少 2 个 chunk 引用的模块
name: "common",
priority: 5, // 全部拆完了,再来看重复代码
},
},
},
},
};
优化技巧
1、分析打包结果
使用 webpack-bundle-analyzer
可视化依赖体积:
npm install --save-dev webpack-bundle-analyzer
const BundleAnalyzerPlugin =
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
module.exports = {
plugins: [new BundleAnalyzerPlugin()],
};
2、提高缓存利用率
为稳定依赖(如 vendor
)设置 contenthash
,尽量减小缓存文件的变化范围,有效的提高浏览器缓存利用率。
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js',
},
3、按需加载 Polyfill
避免全量引入 babel-polyfill
,改用 @babel/preset-env
+ useBuiltIns: 'usage'
。
module.exports = {
// 智能预设:能够编译ES6语法
presets: [
[
"@babel/preset-env",
// 按需加载core-js的polyfill
{ useBuiltIns: "usage", corejs: { version: "3", proposals: true } },
],
],
};
注意事项
- 避免过度拆分:HTTP/2 多路复用下,过多小文件可能增加请求开销。
- 同步与异步 chunk:
SplitChunksPlugin
需配置chunks: 'all'
才能覆盖异步 chunk。 - 服务端配置:确保支持 chunk 文件的按需加载(如 Nginx 正确配置
try_files
)。 - 合理配置缓存:确保公共代码和第三方库的 chunk 能够被浏览器缓存。
- 处理加载状态:动态加载组件时,需提供加载指示(如加载动画)。
通过合理组合上述策略,可显著提升应用加载速度和运行效率。
总结
- 设置代码分割策略
splitChunks: "all"
,不需要关心其他的一些参数。 - 公共模块提取:主要指
common
组件、util
工具函数、layout
布局组件。 - Runtime 文件提取:runtime 代码是 webpack 用来处理模块依赖的辅助代码、胶水代码。
- 第三方依赖(Vendor)分拆:其实就是把
node_module
中较大的包拆出来,如 Element-UI、Vue、Loadash。 - 魔法注释:我们可以用魔法注释
/* webpackChunkName: "name" */
来标记import()
引入的组件、路由等。 - Css 文件提取:使用
MiniCssExtractPlugin
将 css 从 html 里提取出来 - 提取重复代码:
splitChunks.{Vendor}.minChunks
定义被复用次数,默认是 1,代表复用一次就提取出来。 - 多入口拆分:合理拆分业务代码,配置多个入口。
更新日志
e7112
-1于