代码分割目的
单入口打包的代码都在一个文件里,这样导致代码过大,所以要把一些不是马上用到的代码拆分出来,加快首屏速度。

单入口
- 异步导入
原则:又大又后面使用的文件,异步加载。
- math.ts
export function add(num1,num2){
return num1 + num2
}
- a.ts
console.log('aaa')
const a = 'aaa'
export default a
- b.ts
console.log('bbb')
const b = 'bbb'
export default b
- index.ts
import {add} from './math.ts'
add(1,2)
setTimeout(() => {
import('./a.ts').then(res => {
console.log(res)
})
import('./b.ts').then(res => {
console.log(res)
})
},2000)
- 打包之后
多入口
多入口因为有多个入口,所以打包后的代码本来就是分开的。
- 多入口主要去解决重复加载同一段逻辑代码。

- webpack.config.js
module.exports = {
entry:{
app1: "./src/index1.ts",
app2: "./src/index2.ts",
},
output: {
filename: "[name].[hash:4].bundle.js",
path: path.resolve(__dirname, "./build"),
}
}
- /src/index1.ts
import {add} from './math.ts'
import a from './a.ts'
import b from './b.ts'
add(1,2)
console.log(a,b)
- /src/index2.ts
import {add} from './math.ts'
import a from './a.ts'
import b from './b.ts'
add(1,2)
console.log(a,b)
打包之后:
两个打包后的 js 文件中有相同的a和b的代码。
mode: "production",
entry: {
app1: "./src/js/index1.js",
app2: "./src/js/index2.js",
},
output: {
filename: "[name].[hash:4].bundle.js",
// 必须是一个绝对路径
path: path.resolve(__dirname, "./build"),
clean: true
},
optimization: {
splitChunks: {
chunks: "all", // 不管同步异步都拆分
minChunks: 2, // 一个模块重复使用几次,才会分割
minSize: 0, // 大于这个值才会拆分
name: 'common' // 拆分出来的文件名称
}
}
打包之后:
第三方库、webpack 运行时代码单独打包
- 第三方库单独分离文件
optimization: {
splitChunks: {
chunks: "all", // 不管同步异步都拆分
cacheGroups: {
// 第三方包
vendor: {
test: /[\/]node_modules[\/]/,
filename: "vendor.js",
chunks: "all",
minChunks: 1,
minSize: 0
},
// 公共代码
common: {
filename: "common.js",
chunks: "all",
minChunks: 2,
minSize: 0
}
}
}
}
- webpack 运行时代码单独打包
optimization:{
splitChunks:{
chunks: "all", // 不管同步异步都拆分
cacheGroups:{
// 第三方包
vendor:{
test: /[\\/]node_modules[\\/]]/,
filename: "vendor.js",
chunks:"all",
minChunks:1
},
// 公共代码
common:{
filename: "common.js",
chunks:"all",
minChunks:2,
minSize:0
}
}
},
runtimeChunk:{
name:"runtime"
}
}
Webpack中常用的代码分离有三种
- 多入口起点:使用entry配置手动分离代码。
- 防止重复:使用 Entry Dependencies 或者 SplitChunksPlugin 去重和分离代码。
- 动态导入:通过模块的内联函数调用来分离代码。
1. 多入口起点
配置多入口
module.exports = {
entry: {
index: "./src/index.js",
main: "./src/main.js",
},
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "./dist"),
},
};
打包之后生成了 index.bundle.js 和 main.bundle.js,打包后的 html 中引入了两个js 文件。
2.1 Entry Dependencies(入口依赖)
index.js 和 main.js 都依赖 dayjs,如果只进行入口分离,那么打包后的两个 bunlde 都有会有一份 dayjs,正确的处理方式是两个文件共享一个包文件。
const path = require("path");
module.exports = {
entry: {
index: { import: "./src/index.js", dependOn: "dayjs" },
main: { import: "./src/main.js", dependOn: "dayjs" },
dayjs: "dayjs",
},
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "./dist"),
},
};
打包生成了dayjs.bundle.js index.bundle.js main.bundle.js
打包后的 html 中引入了三个资源
<script defer="defer" src="index.bundle.js"></script>
<script defer="defer" src="main.bundle.js"></script>
<script defer="defer" src="dayjs.bundle.js"></script>
2.2 SplitChunks
- 为什么需要 splitChunks?
wepack 设置中有 3 个入口文件:a.js、b.js和c.js,每个入口文件都同步 import 了m1.js,不设置 splitChunks,配置下 webpack-bundle-analyzer 插件用来查看输出文件的内容,打包输出是这样的:
// webpack 和 splitChunks 的初始设置如下
const path = require('path');
const BundleAnalyzerPlugin =
require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
mode: 'development',
entry: {
a: './src/a.js',
b: './src/b.js',
c: './src/c.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
clean: true,
},
optimization: {
splitChunks: {
chunks: 'async',
// 生成 chunk 的最小体积(以 bytes 为单位)。
// 因为演示的模块比较小,需要设置这个。
minSize: 0,
},
},
plugins: [new BundleAnalyzerPlugin()],
};
a.js、b.js、c.js
import m1 from './m1'

- chunks
splitChunks.chunks的作用是指示采用什么样的方式来优化分离 chunks,常用的有三种常用的取值:async、initial和all,async是默认值。
async
chunks: 'async' 的意思是只选择通过 import() 异步加载的模块来分离 chunks。举个例子,还是三个入口文件 a.js、b.js 和 c.js,有两个模块文件 m1.js 和 m2.js,三个入口文件的内容如下:
// a.js
import('./utils/m1');
import './utils/m2';
console.log('some code in a.js');
// b.js
import('./utils/m1');
import './utils/m2';
console.log('some code in a.js');
// c.js
import('./utils/m1');
import './utils/m2';
console.log('some code in c.js');
这三个入口文件对于 m1.js 都是异步导入,m2.js 都是同步导入。打包输出结果如下:
对于异步导入,splitChunks 分离出 chunks 形成单独文件来重用,而对于同步导入的相同模块没有处理,这就是 chunks: 'async' 的默认行为。
- initial
把 chunks 的值改为initial后,再来看下输出结果:
同步的导入也会分离出来了,效果挺好的。这就是 initial与async的区别:同步导入的模块也会被选中分离出来。 - all
加入一个模块文件m3.js,并对入口文件作如下更改:
// a.js
import('./utils/m1');
import './utils/m2';
import './utils/m3'; // 新加的。
console.log('some code in a.js');
// b.js
import('./utils/m1');
import './utils/m2';
import('./utils/m3'); // 新加的。
console.log('some code in a.js');
// c.js
import('./utils/m1');
import './utils/m2';
console.log('some code in c.js');
有点不同的是 a.js 中是同步导入 m3.js,而 b.js 中是异步导入。保持 chunks 的设置为 initial,输出如下:
可以到看 m3.js 单独输出的那个 chunks 是 b 中异步导入的,a 中同步导入的没有被分离出来。也就是在 initial 设置下,就算导入的是同一个模块,但是同步导入和异步导入是不能复用的。
把 chunks 设置为 all,再导出康康:
不管是同步导入还是异步导入,m3.js 都分离并重用了。所以 all 在 initial 的基础上,更优化了不同导入方式下的模块复用。
这里有个问题,发现 webpack 的 mode 设置为 production 的情况下,上面例子中 a.js 中同步导入的 m3.js 并没有分离重用,在 mode 设置为 development 时是正常的。不知道是啥原因
async、initial 和 all 类似层层递进的模块复用分离优化,所以如果考虑体积最优化的输出,那就设 chunks 为 all。
如果配置了 async ,是不会多出来一个包的,只有异步导入才会分包。
- minSize
拆分包的大小, 至少为minSize,如果一个包拆分出来达不到minSize,那么这个包就不会拆分。默认值是 20 kb。 - maxSize
将大于maxSize的包,拆分为不小于minSize的包。maxSize 是要大于等于 minSize 的。 - minChunks
至少被引入的次数,默认是1,如果我们写一个2,但是引入了一次,那么不会被单独拆分。 - cacheGroups
通过cacheGroups,可以自定义 chunk 输出分组。设置test对模块进行过滤,符合条件的模块分配到相同的组。
splitChunks 默认情况下有如下分组:
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
};
意思就是存在两个默认的自定义分组,defaultVendors 和 default,defaultVendors 是将 node_modules 下面的模块分离到这个组。我们改下配置,设置下将 node_modules 下的模块全部分离并输出到 vendors.bundle.js 文件中:
const path = require('path');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
mode: 'development',
entry: {
a: './src/a.js',
b: './src/b.js',
c: './src/c.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
clean: true,
},
optimization: {
splitChunks: {
chunks: 'all',
minSize: 0,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
name: 'vendors',
},
},
},
},
plugins: [new BundleAnalyzerPlugin()],
};
所以根据实际的需求,我们可以利用 cacheGroups 把一些通用业务模块分成不同的分组,优化输出的拆分。
举个栗子,我们现在输出有两个要求:
node_modules下的模块全部分离并输出到vendors.bundle.js文件中。utils/目录下有一系列的工具模块文件,在打包的时候都打到一个utils.bundle.js文件中。
const path = require('path');
const BundleAnalyzerPlugin =
require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
mode: 'development',
entry: {
a: './src/a.js',
b: './src/b.js',
c: './src/c.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
clean: true,
},
optimization: {
splitChunks: {
chunks: 'all',
minSize: 0,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
name: 'vendors',
},
default: {
test: /[\\/]utils[\\/]/,
priority: -20,
reuseExistingChunk: true,
name: 'utils',
},
},
},
},
plugins: [new BundleAnalyzerPlugin()],
};
入口文件调整如下:
// a.js
import React from 'react';
import ReactDOM from 'react-dom';
import('./utils/m1');
import './utils/m2';
console.log('some code in a.js');
// b.js
import React from 'react';
import './utils/m2';
import './utils/m3';
console.log('some code in a.js');
// c.js
import ReactDOM from 'react-dom';
import './utils/m3';
console.log('some code in c.js');

- maxInitialRequests 和 maxAsyncRequests
maxInitialRequests 表示入口的最大并行请求数。规则如下:
- 入口文件本身算一个请求。
import()异步加载不算在内。- 如果同时有多个模块满足拆分规则,但是按
maxInitialRequests的当前值现在只允许再拆分一个,选择容量更大的 chunks。
const path = require('path');
const BundleAnalyzerPlugin =
require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
mode: 'development',
entry: {
a: './src/a.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
clean: true,
},
optimization: {
splitChunks: {
chunks: 'all',
minSize: 0,
maxInitialRequests: 2,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
name: 'vendors',
},
default: {
test: /[\\/]utils[\\/]/,
priority: -20,
reuseExistingChunk: true,
name: 'utils',
},
},
},
},
plugins: [new BundleAnalyzerPlugin()],
};
入口文件内容如下:
// a.js
import React from 'react';
import './utils/m1';
console.log('some code in a.js');
打包输出结果如下:
按照 maxInitialRequests = 2 的拆分过程如下:
- a.bundle.js 算一个文件。
- vendors.bundle.js 和 utils.bundle.js 都可以拆分,但现在还剩一个位,所以选择拆分出 vendors.bundle.js。
把
maxInitialRequests的值设为 3,结果如下:
再来考虑另外一种场景,入口依然是 a.js文件,a.js的内容作一下变化:
// a.js
import './b';
console.log('some code in a.js');
// b.js
import React from 'react';
import './utils/m1';
console.log('some code in b.js');
调整为 a.js 同步导入了 b.js,b.js 里再导入其他模块。这种情况下 maxInitialRequests 是否有作用呢?可以这样理解,maxInitialRequests 是描述的入口并行请求数,上面这个场景 b.js 会打包进 a.bundle.js,没有异步请求;b.js 里面的两个导入模块按照 cacheGroups 的设置都会拆分,那就会算进入口处的并行请求数了。
比如 maxInitialRequests 设置为 2 时,打包输出结果如下:
设置为 3 时,打包输出结果如下:
maxAsyncRequests 的意思是用来限制异步请求中的最大并发请求数。规则如下:
import()本身算一个请求。- 如果同时有多个模块满足拆分规则,但是按
maxAsyncRequests的当前值现在只允许再拆分一个,选择容量更大的 chunks。 还是举个栗子,webpack 配置如下:
const path = require('path');
const BundleAnalyzerPlugin =
require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
mode: 'development',
entry: {
a: './src/a.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
clean: true,
},
optimization: {
splitChunks: {
chunks: 'all',
minSize: 0,
maxAsyncRequests: 2,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
name: 'vendors',
},
default: {
test: /[\\/]utils[\\/]/,
priority: -20,
reuseExistingChunk: true,
name: 'utils',
},
},
},
},
plugins: [new BundleAnalyzerPlugin()],
};
入口及相关文件内容如下:
// a.js
import ('./b');
console.log('some code in a.js');
// b.js
import React from 'react';
import './utils/m1';
console.log('some code in b.js');
这个时候是异步导入 b.js 的,在 maxAsyncRequests = 2 的设置下,打包输出结果如下:
按照规则:
import('.b')算一个请求。- 按 chunks 大小再拆分
vendors.bundle.js。
最后import './utils/m1'的内容留在了b.bundle.js中。如果将maxAsyncRequests = 3则输出如下:
3. 动态导入
使用 ECMAScript 中的 import() 语法来完成,也是目前推荐的方式。 异步导入的代码,都会生成独立文件。
import("./bar_1");
import("./bar_2");
- 动态导入的文件命名
因为动态导入通常是一定会打包成独立的文件的,所以并不会再cacheGroups中进行配置,通常会在output中,通过 chunkFilename 属性来命名。
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "./dist"),
chunkFilename: "chunk_[id]_[name].js"
}
默认情况下我们获取到的 [name] 是和id的名称保持一致的。
如果我们希望修改name的值,可以通过magic comments(魔法注释)的方式。

4.chunkIds
optimization.chunkIds 配置用于告知 webpack 模块的 id 采用什么算法生成。
- natural:按照数字的顺序使用id

- named:development下的默认值,一个可读的名称的id

- deterministic:确定性的,在不同的编译中不变的短数字id

5.代码的懒加载
动态import使用最多的一个场景是懒加载(比如路由懒加载): 封装一个component.js,返回一个component对象,可以在一个按钮点击时,加载这个对象。
const element = document.createElement("div");
element.innerHTML = "cpn";
export default element;
const button = document.createElement("button");
button.innerHTML = "点击按钮";
button.addEventListener("click", () => {
import("./element").then(({ default: component }) => {
document.body.appendChild(component);
});
});
document.body.appendChild(button);
这个方式有一个缺点是:点击的时候下载 js 文件,然后执行 js 文件,过程有点长了,可以使用 Prefetch 预先下载。
6.Prefetch 和 Preload
Prefetch(预下载)
上述案例修改如下:
const button = document.createElement("button");
button.innerHTML = "点击按钮";
button.addEventListener("click", () => {
import(/* webpackPrefetch: true */"./element").then(({ default: component }) => {
document.body.appendChild(component);
});
});
document.body.appendChild(button);

Preload和父bundle一起下载。
Prefetch 和 Preload 区别
- preload chunk 会在父 chunk 加载时,以并行方式开始加载。
- prefetch chunk 会在父 chunk 加载结束后开始加载。
- preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
- preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻.