认识Code Spliting(代码分割)
- 默认情况下,一些第三方库以及一些首页用不到的业务代码很可能都打到相同的bundle中在第一时间加载,那将使首页加载的速度变慢
- 代码分割的主要目的是将代码分割到不同的bundle中,这样就可以对bundle进行选择性的并行加载或者按需加载
webpack中Code Spliting的三种模式
多入口对应多出口
动态导入
抽离代码 splitChunks
重点讲解
多入口对应多出口
先来看单入口
入口处为index.js
js
// index.js
var index = 'index.js'
console.log(index)
var main = 'main.js'
console.log(main)
js
// webpack
...
entry: {
index: getPath('src/code-spliting/index.js'),
},
output: {
path: getPath('dist'),
filename: 'js/[name].[chunkhash:8].bundle.js',
clean: true
},
...
毫无疑问,默认会输出一个bundle也就是index.js对应的内容。
如果想把上述输出内容一分为二,很简单,拆分多入口即可!
多入口
js
// main.js
var main = 'main.js'
console.log(main)
// index.js
var index = 'index.js'
console.log(index)
js
// webpack
...
entry: {
index: getPath('src/code-spliting/index.js'),
main: getPath('src/code-spliting/main.js'),
},
...
看一下输出产物
动态导入
下面新建other1文件,并在index.js中动态引入
webpack内部对于动态导入的模块默认采用单独打包的形式
js
...
entry: {
index: getPath('src/code-spliting/index.js'),
main: getPath('src/code-spliting/main.js'),
},
output: {
path: getPath('dist'),
filename: 'js/[name].[chunkhash:8].bundle.js',
chunkFilename: 'js/[name].bundle.js',
clean: true
},
...
dist输出产物:
就算在main.js
中动态引入也是只会产生一个async-other.bundle.js
splitChunks
webpack官网的默认配置
yaml
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async',
minSize: 20000,
minRemainingSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[\/]node_modules[\/]/,
priority: -10,
reuseExistingChunk: true,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
官网www.webpackjs.com/plugins/spl...
对后面能用到的几个关键的属性解释一下~
- chunks :
async (默认值)
处理异步 导入的代码
initial
处理同步 导入的代码(注意:webpack默认支持异步导入代码的抽离不受影响)
all
同步+异步代码都会进行处理 - minSize:抽离出来的包最小的大小(抽离出来的包特别特别小,比给定的值还要小就没意义了,还要浪费一次http请求)
- minChunks:被多入口引用的次数
- cacheGroups :缓存组,某个模块在满足缓存组的规则之后不会立即抽离,而是等其他也满足该规则的模块打包到一组;(注意:每个缓存组内的属性优先级比外部
splitChunks
下的属性优先级高,外部的是统一的,内部的是针对当前缓存组来说的) - priority: 缓存组的权重,相同规则下权重高的命中
- reuseExistingChunk:某个模块已经被抽离出来,直接服用不会再次打包
小试牛刀
- 入口总共三个文件:
entry.js
,index.js
,main.js
- other文件夹下是要被引入的文件:
other1.js
,other2.js
,other3.js
- 每个文件内部仅仅只打印自身的文件名
initial模式
抽离公共模块
三个入口文件 分别引入other1.js import './other/other1.js'
下面看打包输出结果 三个入口对应输出的三个bundle,而且各自内部都嵌入一份other1的代码,并没有抽离代码。
这是因为默认情况下splitChunks.chunks为async
,只对异步导入做抽离,我们的同步代码并没有抽离,下面尝试改一下配置。
js
// webpack
optimization: {
splitChunks: {
chunks: 'initial',
minSize: 0,
minChunks: 3, // 1;2;3 都可以,因为other1一共被引入了三次
cacheGroups: {
defaultVendors: false, // 取消默认缓存组
default: false // 取消默认缓存组
}
}
},
输出结果不难看出other1
被抽离出来了
抽离三方库
下载一个第三方库lodash
在entry和index中引入,补充cacheGroups
内部配置项
js
optimization: {
splitChunks: {
...
minChunks: 3,
cacheGroups: {
defaultVendors: false, // 取消默认缓存组
default: false, // 取消默认缓存组,
other: {
test: /other/,
filename: 'split/other-[id].bundle.js'
},
vendors: {
test: /node_modules/,
filename: 'split/vendors-[id].bundle.js'
}
}
}
},
看下结果:other1
被正常抽离到split目录下other-832.bundle.js
为啥lodash也是公共代码,但是没有被抽离呢? 是因为缓存组继承外层的minChunks为3,而lodash
被引入了两次。我们把外层的minChunks
(或者vendors缓存组的minChunks
) 改成 2试一试。
果然不出所料!lodash
也抽离成vendors-xxx.bundle.js
了
再思考一下
这里补充一下上面说的把外层minChunks
改成 2,其他没变
js
// webapck
optimization: {
splitChunks: {
...
minChunks: 2,
cacheGroups: {
defaultVendors: false, // 取消默认缓存组
default: false, // 取消默认缓存组,
other: {
test: /other/,
filename: 'split/other-[id].bundle.js'
},
vendors: {
test: /node_modules/,
filename: 'split/vendors-[id].bundle.js'
}
}
}
},
目前三个入口文件(entry,index,main
)都引入了other1.js
,我们在entry.js
和index.js
两个文件引入lodash
的基础上再次同时新增引入other2.js
,显然other2
已经满足other缓存组
的条件了,思考一下other2
会被打入到上面抽出的other1
对应的other-832.bundle.js
中吗?
看下输出结果 很显然除了other-832.bundle.js
还有other-193.bundle.js
,同一规则下两个相互独立的bundle
我的理解是这样的:对于三个入口文件来说other1是公共的 ,而对于entry和index来说other2是公共的,如果都打在一起的情况下对于entry和index是合理的,因为他们都用到了两个模块。但是对于第三个入口main是不合理的只引入了other1一个模块,所以是两个bundle。(在包的大小以及并发引用次数等一系列属性都理想化时)。
如果在main.js
中也引入other2.js
,满足三个入口都引入other1
和other2
,那么会打在一起吗?
很显然是打在一起的
如果在两个入口再引入other3.js
满足other缓存组
的条件呢?答案是和上面第一个问题一样的,产生两个bundle
,other1
和other2
是一个,other3
是一个。
也就是说理想情况下在满足同一缓存组条件的前提下,不同的模块被相同的文件引入可能会产生同一个包,被不同的文件引入产生不同的包
将引入的other打到一起
如果想要把满足缓存组条件
的包都强制打到一起 ,就要设置一个name
,如下代码会将满足other缓存组条件的代码全部打成一个other.bundle.js
js
// webpack
cacheGroups: {
...
other: {
test: /other/,
minChunks: 2,
filename: 'split/other-[name].[id].bundle.js',
name: 'all'
},
...
}
chunks设置initial情况下的异步代码
webpack本身就天然分割异步导入的代码,设置成initial只是对同步代码做出分割,并不会影响异步分割的代码(动态导入)
- 三个入口文件entry.js , index.js , main.js 同步引入
other1.js
;
import './other/other1.js'
- entry.js 异步引入
other2.js
;
import (/* webpackChunkName: "async-other2"*/ './other/other2.js')
- index.js 同步引入
other2.js
import './other/other2.js'
为了方便演示去掉other组内的name属性
js
...
output: {
path: getPath('dist'),
filename: 'js/[name].[chunkhash:8].bundle.js',
chunkFilename: 'js/[name].bundle.js',
clean: true
},
...
other: {
test: /other/,
minChunks: 2,
filename: 'split/other-[id].bundle.js',
// name: 'all'
},
- 对于other1来说,肯定是要抽出来的(熟悉的配方)
- 对于异步引入的other2被独立抽离出来了
- 对于同步引入的other2来说,只在index同步引入了一次,不满足抽离的条件被嵌入到index中了
async模式
采用如下依赖关系:
- 三个入口文件entry.js , index.js , main.js 同步引入
other3.js
;
import './other/other3.js'
- entry.js 异步引入
other1.js
;
import (/* webpackChunkName: "async-other1"*/ './other/other1.js')
- index.js 异步引入
other2.js
import (/* webpackChunkName: "async-other2"*/ './other/other2.js')
splitChunks.chunks
默认值为async
,我们把上述配置中的chunks
改为async
表示该组(other)只匹配异步代码
js
...
output: {
path: getPath('dist'),
filename: 'js/[name].[chunkhash:8].bundle.js',
chunkFilename: 'js/[name].bundle.js',
clean: true
},
...
optimization: {
splitChunks: {
...
...
other: {
test: /other/,
chunks: 'async'
minChunks: 2,
filename: 'split/other-[id].bundle.js',
},
}
}
看下输出结果:
- 同步代码并没有分割出来,这是意料之中的
- 代码中采用异步引入的模块并没有根据缓存组的规则被抽离出来 ,依然采用默认的异步分割的方式,我们把
minChunks
改为 1试一下
js
// webpack
...
other: {
test: /other/,
minChunks: 1,
filename: 'split/other-[id].bundle.js',
},
...
补充name属性,完善filename属性:
js
// webpack
...
other: {
test: /other/,
minChunks: 1,
filename: 'split/other-[name]-[id].bundle.js',
name: 'all'
},
...
果然,对于异步代码来说满足规则的部分会进行抽离,如果不满足则会天然分割成独立的包
all模式
chunks: all
不论是同步引入还是异步引入的代码都会匹配,把chunks
改为'all'
试一下
js
// webpack
...
other: {
test: /other/,
chunks: 'all',
minChunks: 1,
filename: 'split/other-[id].bundle.js',
},
...
other1 other2 other3
都符合other下的规则
同步引入的other3
打成了other-682.bundle.js
,另外两个异步引入的other1
和other2
也都分别打成了other-193-bundle.js
和other-832-bundle.js
如果想把这一组打成一个包,那么就要设置name
属性
js
// webpack
...
other: {
test: /other/,
chunks: 'all',
minChunks: 1,
filename: 'split/other-[name]-[id].bundle.js',
name: 'other-all'
},
...
注意:all,对于同一模块即被同步引入又用异步引入,缓存组设置name会将包合并复用
总结一下
- webpack本身就是天然对异步导入的代码进行独立分割的
- 对于
splitChunks.cacheGroups
会把满足匹配条件的模块都按照该组的方式进行分割(可能打成多个包,也可能打成一个包),如果想把所有满足该组条件的模块全部打成一个包,要给该组设置一个固定的name(可以是一个字符串) chunks: initial
只匹配同步导入代码,满足条件的的同步代码进行分割,不影响天然分割的异步代码块chunks: async
只匹配异步导入代码,对满足条件的异步代码块进行分割,不满足的异步代码依然会被天然分割chunks:all
匹配(同步+异步)导入代码,如果想把满足该组所有的chunk打成一个包,可以设置一个固定name
先写到这儿,路过的大佬有什么宝贵的建议,可以交流探讨一下~