项目分享一千字,读完须要5分钟,实例部分三千字,读完须要15分钟,动手尝试实例须要1小时,若有错误请指正。
复制代码
本项目是淘系用户增加团队的一个大中台系统,单页应用,涵盖不少业务功能,运用了不少懒加载页面组件来提高性能,首屏时间 1s 左右,体验良好。然而大项目文件不少,致使构建和发布时间很长,内存占用较大。个人任务是尽量优化与此相关的问题。前端
const compiler = webpack(webpackConfig); //经过webpack配置信息获得编译器,而后配置其输出信息
compiler.hooks.done.tap('done', (stats) => {
console.log(
stats.toString({
colors: true,
chunks: true,//这里设为true
assets: true,
children: false,
modules: false,
})
);
}
复制代码
固然,简单的项目也能够在打包命令后面加个参数:webpack --display-chunks,效果和上面至关。node
[./src/components/xxx.jsx] 4.52 KiB {55} {60} {66} {73} {87} {96} {113} {119} {127} {129} {133}
[./node_modules/base/yyy.js] 205 bytes {50} {54} {64} {70} {73} {74} {75} {80} {82} {83} {87} {92} {97} {104} {109} {111} {112} {113} {115} {117} {120} {127} {128} {129} {130} {132} {138} {150} {151}
[./node_modules/base/zzz.js] 205 bytes {50} {54} {64} {70} {73} {74} {75} {80} {82} {83} {87} {92} {97} {104} {109} {111} {112} {113} {115} {117} {120} {127} {128} {129} {130} {132} {138} {150} {151}
···
复制代码
每一个大括号内都是一个 chunk 的 id,这三个模块被重复打包到了众多 chunk 中。webpack
制定代码分割策略,着重配置 optimization.splitChunks,提取重复模块,要兼顾首屏性能,首页须要的包不能太大,若是打得太大须要拆分。(项目代码分割策略不便贴出,只能用下面的实例来代替了)git
最后验证打包效果,不断调整策略直至最优。github
摘自个人淘宝前端团队周报:web
项目的打包编译优化,取得有效成果,目前项目整体积比原来减小了 6.4M(原来体积 42.2M,如今 35.8M),编译时间缩短 60% ,发布时间缩短 20%,文件数减小 30 个,打包时再也不出现内存溢出问题。正则表达式
使用的技术只有一个:webpack 的 SplitChunksPlugin。SplitChunksPlugin 出了两年,社区也积累了很多资料,我仍是以为须要补充下面的实例教程,有两个缘由:缓存
如下代码图文预警,webpack 知识体系完整的老司机能够直接略过,小白建议仔细阅读。bash
专心作事前,首先要找准大方向,才不会在复杂项目中迷路。前端优化无外乎作两件事:网络
而 webpack 提供了模块化项目中最主要的优化手段:
因此,咱们就是要经过 Webpack 的两大优化手段,去完成上面前端优化的两件事。当咱们面对庞大的项目摸不着头脑,不妨跳出来看看。
SplitChunksPlugin 引入缓存组(cacheGroups)对模块(module)进行分组,每一个缓存组根据规则将匹配到的模块分配到代码块(chunk)中,每一个缓存组的打包结果能够是单一 chunk,也能够是多个 chunk。
webpack 作了一些通用性优化,咱们手动配置 SplitChunksPlugin 进行优化前,须要先理解 webpack 默认作了哪些优化,是怎么作的,以后才能根据本身的须要进行调整。既然造了 SplitChunksPlugin,本身确定得用上,webpack 的默认优化就是经过 SplitChunksPlugin 配置实现的,以下:
module.exports = {
//...
optimization: {
splitChunks: {
//在cacheGroups外层的属性设定适用于全部缓存组,不过每一个缓存组内部能够重设这些属性
chunks: "async", //将什么类型的代码块用于分割,三选一: "initial":入口代码块 | "all":所有 | "async":按需加载的代码块
minSize: 30000, //大小超过30kb的模块才会被提取
maxSize: 0, //只是提示,能够被违反,会尽可能将chunk分的比maxSize小,当设为0表明能分则分,分不了不会强制
minChunks: 1, //某个模块至少被多少代码块引用,才会被提取成新的chunk
maxAsyncRequests: 5, //分割后,按需加载的代码块最多容许的并行请求数,在webpack5里默认值变为6
maxInitialRequests: 3, //分割后,入口代码块最多容许的并行请求数,在webpack5里默认值变为4
automaticNameDelimiter: "~", //代码块命名分割符
name: true, //每一个缓存组打包获得的代码块的名称
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/, //匹配node_modules中的模块
priority: -10, //优先级,当模块同时命中多个缓存组的规则时,分配到优先级高的缓存组
},
default: {
minChunks: 2, //覆盖外层的全局属性
priority: -20,
reuseExistingChunk: true, //是否复用已经从原代码块中分割出来的模块
},
},
},
},
};
复制代码
其中五个属性是控制代码分割规则的关键,我再额外提一提:
这些规则一旦制定,只有所有知足的模块才会被提取,因此须要根据项目状况合理配置才能达到满意的优化结果。
name(默认为 true),用来决定缓存组打包获得的 chunk 名称,容易被轻视但做用很大。奇特的是它有两种类型取值,boolean 和 string:
该上手撸代码了。webpack 默认优化策略对普通规模的项目已然足够,然而大厂的项目几经转手一般错综复杂,这时就须要手动优化了。下面咱们经过一个有必定结构复杂度的实例来玩转 SplitChunksPlugin,当前版本的 webpack 是 4.43.0,项目结构以下:
|--node_modules/
| |--vendor1.js
| |--vendor2.js
|--pageA.js
|--pageB.js
|--pageC.js
|--utility1.js
|--utility2.js
|--utility3.js
|--webpack.config.js
复制代码
vendor1.js:
export default () => {
console.log("vendor1");
};
复制代码
vendor2.js:
export default () => {
console.log("vendor2");
};
复制代码
pageA.js:
import vendor1 from "vendor1";
import utility1 from "./utility1";
import utility2 from "./utility2";
export default () => {
console.log("pageA");
};
复制代码
pageB.js:
import vendor2 from "vendor2";
import utility2 from "./utility2";
import utility3 from "./utility3";
export default () => {
console.log("pageB");
};
复制代码
pageC.js:
import utility2 from "./utility2";
import utility3 from "./utility3";
export default () => {
console.log("pageC");
};
复制代码
utility1.js:
import utility2 from "./utility2";
export default () => {
console.log("utility1");
};
复制代码
utility2.js:
export default () => {
console.log("utility2");
};
复制代码
utility3.js:
export default () => {
console.log("utility3");
};
复制代码
每一个文件内容并很少,关键在于它们的引用关系。webpack.config.js 配置以下:
var path = require("path");
module.exports = {
mode: "development",
// mode: "production",
entry: {
pageA: "./pageA",
pageB: "./pageB",
pageC: "./pageC",
},
optimization: {
chunkIds: "named", // 指定打包过程当中的chunkId,设为named会生成可读性好的chunkId,便于debug
splitChunks: {
minSize: 0, // 默认30000(30kb),可是demo中的文件都很小,minSize设为0,让每一个文件都知足大小条件
cacheGroups: {
commons: {
chunks: "initial",
minChunks: 2,
maxInitialRequests: 3, // 默认为3
},
vendor: {
test: /node_modules/,
chunks: "initial",
name: "vendor",
},
},
},
},
output: {
path: path.join(__dirname, "dist"),
filename: "[name].js",
},
};
复制代码
控制台运行:webpack,打包结果:
commons: {
chunks: "initial",
minChunks: 2,
maxInitialRequests: 5, // 默认为3时,没法知足咱们的分包数量
},
复制代码
再次打包,结果为:
咱们继续研究,不由产生疑问:为何缓存组 commons 产出 commons~pageA~pageB~pageC.js 和 commons~pageB~pageC.js,而缓存组 vendor 产出 vendor.js?包名格式相差巨大。这就是 name 属性在起做用,咱们注释掉 vendor 中的 name:
vendor: {
test: /node_modules/,
chunks: "initial",
// name: "vendor",
}
复制代码
打包结果以下:
splitChunks: {
minSize: 0,// 默认30000(30kb),可是demo中的文件都很小,minSize设为0,让每一个文件都知足大小条件
name:false,
cacheGroups: {
commons: {
chunks: "initial",
minChunks: 2,
maxInitialRequests: 5, // 默认为3时,没法知足咱们的分包数量
},
vendor: {
test: /node_modules/,
chunks: "initial",
name: "vendor",
}
}
}
复制代码
打包结果以下:
上面的实例都是针对入口文件的优化,如今混入按需加载代码,看看会给咱们的优化带来什么新体验。项目中加入两个懒加载文件,async1.js 和 async2.js:
|--node_modules/
| |--vendor1.js
| |--vendor2.js
|--pageA.js
|--pageB.js
|--pageC.js
|--utility1.js
|--utility2.js
|--utility3.js
|--async1.js
|--async2.js
|--webpack.config.js
复制代码
async1.js 代码:
import utility1 from "./utility1";
export default () => {
console.log("async1");
};
复制代码
async2.js 代码:
import utility1 from "./utility1";
export default () => {
console.log("async1");
};
复制代码
pageA.js 更新为:
import vendor1 from "vendor1";
import utility1 from "./utility1";
import utility2 from "./utility2";
export default () => {
//懒加载
import("./async1");
import("./async2");
console.log("pageA");
};
复制代码
webpack.config.js 配置为:
var path = require("path");
module.exports = {
mode: "development",
// mode: "production",
entry: {
pageA: "./pageA",
pageB: "./pageB",
pageC: "./pageC",
},
optimization: {
chunkIds: "named", // 指定打包过程当中的chunkId,设为named会生成可读性好的chunkId,便于debug
splitChunks: {
minSize: 0, // 默认30000(30kb),可是demo中的文件都很小,minSize设为0,让每一个文件都知足大小条件
// name:false,
cacheGroups: {
commons: {
chunks: "all", //加入按需加载后,设为all将全部模块包括在优化范围内
// name: "commons",
minChunks: 2,
maxInitialRequests: 5, // 默认为3,没法知足咱们的分包数量
},
vendor: {
test: /node_modules/,
chunks: "initial",
name: "vendor",
},
},
},
},
output: {
path: path.join(__dirname, "dist"),
filename: "[name].js",
},
};
复制代码
其余代码不变。如今项目文件又增长了,直接 webpack 打包输出的信息可能不太够用,咱们执行:webpack --display-chunks,具体获知每一个 chunk 包含哪些包含哪些模块,属于哪一个缓存组,这样就能够根据 chunk 的具体信息,判断是否有重复模块没提取干净,是否有一些模块明没有命中咱们想要的规则,若是有,表明还有继续优化的空间。打包结果以下:
观察结果咱们发现,utility1.js 同时被 pageA.js,async1.js,async2.js 三个模块引用,照理应该命中 commons 缓存组的规则,从而被单独提取成一个 chunk,然而结果是它依然打包在 pageA.js 中。这是由于 async1.js,async2.js 都是 pageA.js 的懒加载模块,而 pageA.js 同步引用了 utility1.js,因此在加载 async1.js,async2.js 时 utility1.js 已经有了,直接拿来用便可,因此就不必提出一个新的 chunk,白白增长一个请求。
那什么状况下 utility1.js 才会被单独提出来?咱们调整代码,将按需加载代码从 pageA.js 移到 pageB.js:
pageA.js:
import vendor1 from "vendor1";
import utility1 from "./utility1";
import utility2 from "./utility2";
export default () => {
//懒加载
// import('./async1');
// import('./async2');
console.log("pageA");
};
复制代码
pageB.js:
import vendor2 from "vendor2";
import utility2 from "./utility2";
import utility3 from "./utility3";
export default () => {
//懒加载
import("./async1");
import("./async2");
console.log("pageB");
};
复制代码
执行 webpack --display-chunks,结果以下:
最后咱们想把数字 id 名称变成有意义的名称,可使用 webpack 的 magic comments,把 pageB.js 改成:
import vendor2 from "vendor2";
import utility2 from "./utility2";
import utility3 from "./utility3";
export default () => {
//懒加载
import(/* webpackChunkName: "async1" */ "./async1");
import(/* webpackChunkName: "async2" */ "./async2");
console.log("pageB");
};
复制代码
普通打包便可,结果为:
这个实例运用了 webpack 两样主要优化手段,主要聚焦于如何让项目打包处在咱们的掌控之中,不至于出现没法理解的打包状况,最终获得想要的打包结果。但愿读完本文,你们面对再复杂的项目都能有优化入手点。
固然,优化自己是一件拆东补西的事,好比提取出一个公共 chunk,打包产出的文件就会多一个,也必然会增长一个网络请求。当项目很庞大,每一个公共模块单独提取成一个 chunk 会致使打包速度出奇的慢,影响开发体验,因此一般会取折衷方案,将重复的较大模块单独提取,而将一些重复的小模块打包到一个 chunk,以减小包数量,同时不能让这个包太大,不然会影响页面加载时间。
在淘宝研究了一段时间打包的事儿,把个人心得分享给你们:优化就是在有限的时间空间和算力下,去除低效的重复(提出公共大模块),进行合理的冗余(小文件容许重复),并利用一些用户无感知的区间(预加载),达到时间和空间综合考量上的最优。
下一期,一块儿走进 SplitChunksPlugin 源码,条分缕析 webpack 的代码分割原理。 没多少人吧,趁机立个flag:点赞超50,一周内直接SplitChunksPlugin源码撕出来。
--- 分割线 ---
谢谢你们捧场,本身立的flag跪着也要拔了,下一期在整了哈(指新建文件夹[dog])