webpack优化
文章目录
代码分离
认识代码分离
代码分离(Code Splitting)是webpack一个非常重要的特性:
- 它主要的目的是将代码分离到不同的bundle中,之后我们可以按需加载,或者并行加载这些文件;
- 比如默认情况下,所有的JavaScript代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载, 就会影响首页的加载速度;
- 代码分离可以分出出更小的bundle,以及控制资源加载优先级,提供代码的加载性能;
Webpack中常用的代码分离有三种:
- 入口起点:使用entry配置手动分离代码;
- 防止重复:使用Entry Dependencies或者SplitChunksPlugin去重和分离代码;
- 动态导入:通过模块的内联函数调用来分离代码;
多入口起点
入口起点的含义非常简单,就是配置多入口:
-
比如配置一个index.js和main.js的入口;
-
他们分别有自己的代码逻辑;
Entry Dependencies(入口依赖)
假如我们的index.js和main.js都依赖两个库:lodash、dayjs
-
如果我们单纯的进行入口分离,那么打包后的两个bunlde都有会有一份lodash和dayjs;
-
事实上我们可以对他们进行共享;
配置很灵活,import也可以是再次多入口,是多个文件。
SplitChunks
另外一种分包的模式是splitChunk,它是使用SplitChunksPlugin来实现的:
- 因为该插件webpack已经默认安装和集成,所以我们并不需要单独安装和直接使用该插件;
- 只需要提供SplitChunksPlugin相关的配置信息即可;
Webpack提供了SplitChunksPlugin默认的配置,我们也可以手动来修改它的配置:
比如默认配置中,chunks仅仅针对于异步(async)请求,我们可以设置为initial或者all;
chunks
- async:当设置chunks的值为async时,只有在异步加载模块的时候,才会进行分包处理该模块
- initial:同步加载模块的时候,也会进行分包处理。
- all:同步异步都会进行分包处理
- 默认值是async
- 另一个值是initial,表示对通过的代码进行处理
- all表示对同步和异步代码都进行处理**(最为常用)**
其他的splitChunks属性(很少手动配置)
minSize和maxSize
minSize的优先级高于maxSize,
- minSize:拆分出的包的大小,至少为该minSize的值,默认值是20000B大小,如果需要拆分的包的大小不足该值的大小,是不会进行分包的。
- 拆分包的大小, 至少为minSize;
- 如果一个包拆分出来达不到minSize,那么这个包就不会拆分;
- maxSize:将大于maxSize的包,再次拆分为大于minSize,但是不会大于maxSize的包。
minChunks
在模块中,使用(import,require)等关键字引入其他模块的时候,只有引入次数大于等于该值时,才会进行分包。
- 至少被引入的次数,默认是1;
- 如果我们写一个2,但是引入了一次,那么不会被单独拆分
cacheGroups
缓存组,出现在缓存组中的模块,不会直接分包,而是在所有模块加载完毕以后再根据缓存组配置的内容进行分包处理。
- 用于对拆分的包就行分组,比如一个lodash在拆分之后,并不会立即打包,而是会等到有没有其他符合规则的包一起来打 包
- test属性:匹配符合规则的包;
- name属性:拆分包的name属性;
- filename属性:拆分包的名称,可以自己使用placeholder属性;
// 优化
optimization: {
// 代码压缩操作
minimizer: [
new TerserPlugin({
// 去除注释信息
extractComments: false,
}),
],
splitChunks: {
chunks: "all",
// chunks:"async" // 默认值 模块存在异步加载操作(import("文件")) 进行分离
// 最小值 默认值:20000B ~ 拆分出来的最小的包的大小是 大概20KB
minSize: 20,
maxSize: 40000,
minChunks: 1,
cacheGroups: {
// 第三方库 将匹配到的 node_modules下加载的库 都打包到vendors下面
vendors: {
test: /[\\\/]node_modules[\/\\]/,
filename: "[id]_vendors.js",
},
// 将自己的 utils文件夹下的文件 打包
format:{
test:/[\\\/]utils[\/\\]/,
filename:"[id]_utils_format.js"
}
},
},
},
默认缓存组
cacheGroups: {
// 第三方库 将匹配到的 node_modules下加载的库 都打包到vendors下面
vendors: {
test: /[\\\/]node_modules[\/\\]/,
filename: "[id]_vendors.js",
},
// 默认缓存组 当一个文件被引入超过两次的时候 也分包成一个文件
default: {
minChunks: 2,
filename:"[id]_default.js"
},
}
缓存组优先级
如果一个模块同时满足多个缓存组,那么就将模块分包到优先级高的缓存组中。优先级可以为负数。
如图所示:
cacheGroups: {
// 第三方库 将匹配到的 node_modules下加载的库 都打包到vendors下面
vendors: {
test: /[\\\/]node_modules[\/\\]/,
filename: "[id]_vendors.js",
},
// 将自己的 utils文件夹下的文件 打包
format: {
test: /[\\\/]utils[\/\\]/,
filename: "[id]_utils_format.js",
priority: -30,
},
// 默认缓存组 当一个文件被引入超过两次的时候 也分包成一个文件
default: {
minChunks: 2,
filename: "[id]_default.js",
priority: -20,
},
}
整个项目中,只要某个模块的引用次数超过2次(可以等于),也就是说多入口的引入也是一样,都会打包到默认中。
maxAsyncRequests
最大的异步请求数量。默认值 20
name
设置拆包的名称:
- 可以设置一个名称,也可以设置为false
- 设置为false,则需要在cacheGroups中设置名称
chunkIds
告诉webpack,配置分包的时候,生成的分包文件的id
采用什么算法。
optimization.chunkIds配置用于告知webpack模块的id采用什么算法生成。
-
有三个比较常见的值:
- natural:按照数字的顺序使用id;
- named:development下的默认值,一个可读的名称的id;
- deterministic:确定性的,在不同的编译中不变的短数字id 。确定的文件名一定有确定的短数字id。
- 在webpack4中是没有这个值的;
- 那个时候如果使用natural,那么在一些编译发生变化时,就会有问题
-
最佳实践:
- 开发过程中,我们推荐使用named;
- 打包过程中,我们推荐使用deterministic;
optimization:{
// 采用自然数
chunkIds:"natural"
}
实际开发中很少使用:
因为:
- 不见名知意
- 不利于浏览器缓存
- 如果我们有很多文件,但是有一天删除了生成自然数为1的那个源文件,重新打包,后面的所有文件名都会发生改变,本来并没有修改的代码,因为文件名发生改变,浏览器需要重新请求,无法利用之前的请求缓存。
而named属性值用的较多。在开发环境中很常见
默认情况下,不配该属性值,打包环境的值就是 deterministic
optimization. runtimeChunk
配置runtime相关的代码是否抽取到一个单独的chunk中:
- runtime相关的代码指的是在运行环境中,对模块进行解析、加载、模块信息相关的代码;
- 比如我们的component、bar两个通过import函数相关的代码加载,就是通过runtime代码完成的;
抽离出来后,有利于浏览器缓存的策略:
-
比如我们修改了业务代码(main),那么runtime和component、bar的chunk是不需要重新加载的;
-
比如我们修改了component、bar的代码,那么main中的代码是不需要重新加载的;
设置的值:
- true/multiple:针对每个入口打包一个runtime文件;
- single:打包一个runtime文件;
- 对象:name属性决定runtimeChunk的名称;
optimization:{
chunkIds:"deterministic",
runtimeChunk:{
name:"runtime"
}
}
- 每个动态模块单独打包 ture/“multiple”
- 将所有的动态模块打包到一个文件 “single”
- 设置为一个对象,使用name属性来决定打包文件的名称(就是预占位的[name]的名字)不包含后缀等
通过将 optimization.runtimeChunk
设置为 object
,对象中可以设置只有 name
属性,其中属性值可以是名称或者返回名称的函数,用于为 runtime chunks 命名。
默认值是 false
:每个入口 chunk 中直接嵌入 runtime。
module.exports = {
//...
optimization: {
runtimeChunk: {
// name: "runtime.module", // single的别名配置
// name: (entrypoint) => "runtime.module" // single的别名配置
name: (entrypoint) => `runtime.module-${entrypoint.name}`, // true multiple 的别名配置
}
},
};
动态导入(dynamic import)
同步代码的分包,一般我们最多分成四个文件:
- main.bundle.js
- 第三方库.bundle.js(vendor.chunk.js)
- 多次引入的模块,都打包为 common.chunks.js
- runtime.js
对于异步导入的模块,不管你设置什么样的值,chunks的值为什么,webpack都会在打包时帮我们进行分离。
即使chunks的值为initial,也只是表示splitChunks不对异步代码做分包,webpack依然会帮我们分包。
另外一个代码拆分的方式是动态导入时,webpack提供了两种实现动态导入的方式:
- 第一种,使用ECMAScript中的 import() 语法来完成,也是目前推荐的方式;
- 第二种,使用webpack遗留的 require.ensure,目前已经不推荐使用;
比如我们有一个模块 bar.js:
- 该模块我们希望在代码运行过程中来加载它(比如判断一个条件成立时加载);
- 因为我们并不确定这个模块中的代码一定会用到,所以最好拆分成一个独立的js文件;
- 这样可以保证不用到该内容时,浏览器不需要加载和处理该文件的js代码;
- 这个时候我们就可以使用动态导入;
注意:使用动态导入bar.js:
- 在webpack中,通过动态导入获取到一个对象;
- 真正导出的内容,在该对象的default属性中,所以我们需要做一个简单的解构;
异步导入的模块,不管文件的大小,都会进行分包处理的。
动态导入的文件命名
动态导入的文件命名:
-
因为动态导入通常是一定会打包成独立的文件的,所以并不会再cacheGroups中进行配置;
-
那么它的命名我们通常会在output中,通过 chunkFilename 属性来命名;(设置异步加载的打包文件名)
output:{ chunkFilename:"[name].chunk.js" }
output: {
// 异步 分离打包的文件名称
// 默认情况下:这里的占位name就是我们chunkIds生成的 id
chunkFilename: "[name].chunk.js",
// 以入口文件名称作为打包后文件名称前缀
filename: "[name].bundle.js",
path: path.resolve(__dirname, "dist"),
}
-
你会发现默认情况下我们获取到的 [name] 是和id的名称保持一致的,如果我们希望修改name的值,可以通过**magic comments(魔法注释)**的方式;
// 魔法注释: name的值需要加引号
import(/* webpackChunkName:"bar" */ "./bar").then((res) => {
console.log(res, res.default);
});
代码懒加载
动态import使用最多的一个场景是懒加载(比如路由懒加载):
- 封装一个component.js,返回一个component对象;
- 我们可以在一个按钮点击时,加载这个对象;
这种方法解决了首屏页面暂时用不到的js文件的获取,是加快首屏页面渲染速度的方式之一:
但是也有弊端:
- 代码的懒加载,导致了只有在我们用到该文件的时候才会获取
- 如果文件特别大,那么发起请求获取文件也是比较耗时的
- 当浏览器获取到文件后还需要进行解析
- 可能导致用户在某个操作后,导致很长时间无法看见效果,对用户体验不好
我们如何避免这种情况?
我们可以让首屏渲染完毕后,在浏览器空闲的时候,提前帮我们把需要懒加载的一些文件提前下载好,在用户执行某些操作后,就不需要再次发起请求,直接解析代码即可。
在webpack中,做这种效果很简单:
我们只需要使用魔法注释:webpack就会在浏览器的空闲时间帮我们下载好:
魔法注释
魔法注释:prefetch预获取
只需要在需要提前懒加载的文件前面使用魔法注释:webpackPrefetch:true
,webpack就会帮我们做好。
btn.addEventListener("click", async () => {
const { default: div } = await import(
/* webpackChunkName: "component" */
/* webpackPrefetch: true */
"./components/component"
);
document.body.appendChild(div);
});
可以发现该组件是从预获取的缓存中获取的,而不是再次发起请求。
webpackPreload 预加载
-
预加载会和当前懒加载所在的模块一起,以并行的方式开始加载。而预获取是当父模块加载结束后开始加载。
-
preload是中等优先级,且立即开始下载。prefetch chunk 在浏览器闲置时下载。
-
preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
-
浏览器支持程度不同。
推荐组件等的懒加载,使用prefetch