Webpack优化知识点
# webpack 优化分析
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
- 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
- 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
- 确定入口:根据配置中的 entry 找出所有的入口文件
- 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
- 完成模块编译:在经过上一步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统;
- 开始打包,我们需要获取所有的依赖模块(开始编译及确定入口) .搜索所有的依赖项,这需要占用一定的时间,即搜索时间,那么我们就确定了:我们需要优化的第一个时间就是搜索时间(Webpack 优化构建速度-缩小文件搜索范围)。
- 解析所有的依赖模块(解析成浏览器可运行的代码) .webpack 根据我们配置的 loader 解析相应的文件。日常开发中我们需要使用 loader 对 js ,css ,图片,字体等文件做转换操作,并且转换的文件数据量也是非常大。由于 js 单线程的特性使得这些转换操作不能并发处理文件,而是需要一个个文件进行处理。 我们需要优化的第二个时间就是解析时间,(Webpack 优化构建速度-减少编译次数及并发打包)。
- 将所有的依赖模块打包到一个文件. 将所有解析完成的代码,打包到一个文件中,为了使浏览器加载的包更新(减小白屏时间),所以 webpack 会对代码进行优化。
JS 压缩是发布编译的最后阶段,通常 webpack 需要卡好一会,这是因为压缩 JS 需要先将代码解析成 AST 语法树,然后需要根据复杂的规则去分析和处理 AST,最后将 AST 还原成 JS,这个过程涉及到大量计算,因此比较耗时,打包就容易卡住。我们需要优化的第三个时间就是压缩时间。(优化输出质量-压缩文件体积)
- 二次打包 当更改项目中一个小小的文件时,我们需要重新打包,所有的文件都必须要重新打包,需要花费同初次打包相同的时间,但项目中大部分文件都没有变更,尤其是第三方库。 我们需要优化的第四个时间就是二次打包时间。(缓存)
# webpack 优化
量化
有时,我们以为的优化是负优化,这时,如果有一个量化的指标可以看出前后对比,那将会是再好不过的一件事。 speed-measure-webpack-plugin 插件可以测量各个插件和 loader 所花费的时间,使用之后,构建时,会得到类似下面这样的信息: 对比前后的信息,来确定优化的效果。 speed-measure-webpack-plugin 的使用很简单,可以直接用其来包裹 Webpack 的配置:
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const config = {
//...webpack配置
};
module.exports = smp.wrap(config);
2
3
4
5
6
7
8
# 1. 缩小文件的搜索范围
resolve.modules 配置
resolve.modules 用于配置 Webpack 去哪些目录下寻找第三方模块。resolve.modules 的默认值是[node modules],含义是先去当前目录的/node modules 目录下去找我们想找的模块,如果没找到,就去上一级目录../node modules 中找,再没有就去../ .. /node modules 中找,以此类推,这和 Node.js 的模块寻找机制很相似。当安装的第三方模块都放在项目根目录的./node modules 目录下时,就没有必要按照默认的方式去一层层地寻找,可以指明存放第三方模块的绝对路径,以减少寻找。
resolve: {
// 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
'vant': [path.resolve(__dirname,'node_modules')],
'cd-ui': [path.resolve(__dirname,'./src/plugin')]
},
2
3
4
5
- resolve.alias 配置
通过配置 alias 快速到达指定文件的所在地。如果是针对第三方库做简写会影响 Tree-Shaking,适合对整体性比较强的库使用,如果是像 lodash 这类工具类的比较分散的库,比较适合 Tree-Shaking,避免使用这种方式影响到按需引入。
alias: {
'@': resolve('src'),
},
2
3
- exclude/include 配置
可以通过 exclude/include 配置来确保转译尽可能少的文件。excldue 指定要排除的文件,include 指定要包含的文件。 exclude 的优先级高于 include,在 include 和 exclude 中使用绝对路径数组,尽量避免 exclude,更倾向于使用 include。 建议给 loader 指定 include 或是 exclude,指定其中一个即可,因为 node_modules 目录通常不需要我们去编译,排除后,有效提升编译效率。
const path = require("path");
module.exports = {
module: {
rules: [
{
test: /\.jsx?$/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
include: [path.resolve(__dirname, "src")],
excludes: ["xxx"], // 减少基础模块编译次数
},
],
},
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- resolve.extensions 配置
合理配置 resolve.extensions,减少文件查找 默认值:extensions:['.js', '.json'],当导入语句没带文件后缀时,Webpack 会根据 extensions 定义的后缀列表进行文件查找,对于书写 extensions,我们尽量遵循以下规则:
- 列表值尽量少
- 频率高的文件类型的后缀写在前面
- 源码中的导入语句尽可能的写上文件后缀,如 require(./data)要写成 require(./data.json)
module.noParse 配置
告诉 Webpack 不必解析哪些文件,从而提高构建性能。对于一些没有采用模块化(AMD/CommonJS 规范版本)或是本身已经进行了压缩的文件来说,对他们在进行压缩既耗时且没有意义。我们可以使用 noParse 来标识这个模块,这样 Webpack 会引入这些模块,但是不进行转化和解析,从而提升 Webpack 的构建性能 ,例如:jquery 、lodash。用法:
module.exports = {
//...
module: {
// 使用正则表达式
noParse: /jquery|lodash/;
// 使用函数,从 Webpack3.0.0开始支持
noParse: (content) => {
// 返回true或false
return /jquery|element-ui/.test(content);
};
},
};
2
3
4
5
6
7
8
9
10
11
12
13
# 2. 减少编译次数及并发打包
- 利用缓存如 cache-loader 做打包缓存
在一些性能开销较大的 loader 之前添加 cache-loader,将结果缓存中磁盘中。默认保存在 node_modueles/.cache/cache-loader 目录下。再下次打包的时候对于重复的代码则会被复用。
- npm install cache-loader -D
module.exports = {
module: {
rules: [
{
test: /\.jsx?$/,
use: ["cache-loader", "babel-loader"],
},
],
},
};
2
3
4
5
6
7
8
9
10
与其功能类似的还有:
- HardSourceWebpackPlugin 为模块提供中间缓存,缓存默认的存放路径是: node_modules/.cache/hard-source。HardSourceWebpackPlugin 建议不要和 cache-loader 一起使用,初始化的时候会产生创建缓存等开销,如果两个一起使用,相当于对很多同样的内容做了两次缓存。
- babel-loader 的 cacheDirectory 标志
如果你跟我一样,只打算给 babel-loader 配置 cache 的话,也可以不使用 cache-loader,给 babel-loader 增加选项 cacheDirectory。 cacheDirectory:默认值为 false。当有设置时,指定的目录将用来缓存 loader 的执行结果。之后的 Webpack 构建,将会尝试读取缓存,来避免在每次执行时,可能产生的、高性能消耗的 Babel 重新编译过程。设置空值或者 true 的话,使用默认缓存目录:node_modules/.cache/babel-loader。开启 babel-loader 的缓存和配置 cache-loader,我比对了下,构建时间很接近。
- DllPlugin 减少基础模块编译次数
DllPlugin 动态链接库插件,其原理是把网页依赖的基础模块抽离出来打包到 dll 文件中,当需要导入的模块存在于某个 dll 中时,这个模块不再被打包,而是去 dll 中获取。 为什么会提升构建速度呢? 原因在于 dll 中大多包含的是常用的第三方模块,如 react、react-dom,所以只要这些模块版本不升级,就只需被编译一次。我认为这样做和配置 resolve.alias 和 module.noParse 的效果有异曲同工的效果。
- happypack 或是 thread-loader(webpack4 之后) 多进程解析处理文件
当有大量的文件需要解析和处理的时候,wbepack 的构建会变得很慢。HappyPack 可以把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。让 webpack 在同一时刻处理多个任务,从而提升构建速度。
const Happypack = require("happypack");
module.exports = {
//...
module: {
rules: [
{
test: /\.js[x]?$/,
use: "Happypack/loader?id=js",
include: [path.resolve(__dirname, "src")],
},
{
test: /\.css$/,
use: "Happypack/loader?id=css",
include: [
path.resolve(__dirname, "src"),
path.resolve(__dirname, "node_modules", "bootstrap", "dist"),
],
},
],
},
plugins: [
new Happypack({
id: "js", //和rule中的id=js对应
//将之前 rule 中的 loader 在此配置
use: ["babel-loader"], //必须是数组
}),
new Happypack({
id: "css", //和rule中的id=css对应
use: ["style-loader", "css-loader", "postcss-loader"],
}),
],
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
如果使用的是 thread-loader, 把它放置在其他 loader 之前, 放置在 thread-loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行。 thread-loader 的一些限制
- 这些 loader 不能产生新的文件。
- 这些 loader 不能使用定制的 loader API(也就是说,通过插件)。
- 这些 loader 无法获取 webpack 的选项设置。
module.exports = {
// ...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
// 创建一个 js worker 池
use: ["thread-loader", "babel-loader"],
},
{
test: /\.s?css$/,
exclude: /node_modules/,
// 创建一个 css worker 池
use: [
"style-loader",
"thread-loader",
{
loader: "css-loader",
options: {
modules: true,
localIdentName: "[name]__[local]--[hash:base64:5]",
importLoaders: 1,
},
},
"postcss-loader",
],
},
],
},
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
- terser-webpack-plugin 默认开启多进程压缩优化 缓存文件在 node-modules/.cache 里。
webpack4 中 webpack.optimize.UglifyJsPlugin 已被废弃。webpack4 默认内置使用 terser-webpack-plugin 插件压缩优化代码,而该插件使用 terser 来缩小 JavaScript。
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
parallel: true,
}),
],
},
};
2
3
4
5
6
7
8
9
- IgnorePlugin 忽略第三方指定目录 当引入一些第三方包的时候,有的包经常会带有没有压缩的文件、各国语言的文件,比如 Select2 就会带上一排国家语言包 OwO,这个时候我们就可以使用 IgnorePlugin 在打包时忽略 本地化内容。 例如
module.exports = {
//...
plugins: [
//忽略 moment 下的 ./locale 目录
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
new webpack.IgnorePlugin(/^\.\/dist/js/i18n$/, /select2$/)
]
}
// 在引入时:
import moment from 'moment';
import 'moment/locale/zh-cn';// 手动引入语言包
import select2 from 'select2';
import 'select2/dist/js/i18n/zh-cn';// 手动引入语言包
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- 抽离公共代码 -- 详看打包配置
将公共代码放置 common 目录、ui 框架提取、从不同的入口文件抽取需要的公共代码、抽取 webpack 运行资源.在浏览器运行时,webpack 用来连接模块化的应用程序的所有代码。runtime 包含:在模块交互时,连接模块所需的加载和解析逻辑。包括浏览器中的已加载模块的连接,以及懒加载模块的执行逻辑。
module.exports = {
optimization: {
splitChunks: {
//分割代码块
chunks: "all", // (默认是 async) :initial、async 和 all
minSize: 30000, // (默认是 30000):形成一个新代码块最小的体积
minChunks: 1, // (默认是 1)在分割之前,这个代码块最小应该被引用的次数(译注:保证代码块复用性,默认配置的策略是不需要多次引用也可以被分割)
cacheGroups: {
vendor: {
//第三方依赖
priority: 1, //设置优先级,首先抽离第三方模块
name: "vendor",
test: /node_modules/,
chunks: "initial",
minSize: 0,
minChunks: 1, //最少引入了1次
},
//缓存组
common: {
//公共模块
chunks: "initial",
name: "common",
minSize: 100, //大小超过100个字节
minChunks: 3, //最少引入了3次
},
},
},
runtimeChunk: {
name: "manifest",
},
},
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- 使用 Tree Shaking 剔除 JS 无用代码
tree-shaking 依赖 ES6 的 import、export 的模块化语法.它正常工作的前提是代码必须采用 ES6 的模块化语法,因为 ES6 模块化语法是静态的(在导入、导出语句中的路径必须是静态字符串,且不能放入其他代码块中。如果采用了 ES5 中的模块化,例如 module.export = {...}、require( x+y )、if (x) { require( './util' ) },则 Webpack 无法分析出可以剔除哪些代码。下面启动 tree-shaking
- 修改.babelrc 以保留 ES6 模块化语句:
{
"presets": [
[
"env",
{ "module": false } //关闭Babel的模块转换功能,保留ES6模块化语法
]
]
}
2
3
4
5
6
7
8
- 启动 webpack 时带上 --display-used-exports 可以在 shell 打印出关于代码剔除的提示
- 在使用第三方库时,需要配置 resolve.mainFields: ['jsnext:main', 'main'] 以指明解析第三方库代码时,采用 ES6 模块化的代码入口
# exclude/include
可以通过 exclude/include 配置来确保转译尽可能少的文件。excldue 指定要排除的文件,include 指定要包含的文件。 exclude 的优先级高于 include,在 include 和 exclude 中使用绝对路径数组,尽量避免 exclude,更倾向于使用 include。 建议给 loader 指定 include 或是 exclude,指定其中一个即可,因为 node_modules 目录通常不需要我们去编译,排除后,有效提升编译效率。
const path = require("path");
module.exports = {
module: {
rules: [
{
test: /\.jsx?$/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
include: [path.resolve(__dirname, "src")],
},
],
},
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# cache-loader
在一些性能开销较大的 loader 之前添加 cache-loader,将结果缓存中磁盘中。默认保存在 node_modueles/.cache/cache-loader 目录下。
- npm install cache-loader -D
module.exports = {
module: {
rules: [
{
test: /\.jsx?$/,
use: ["cache-loader", "babel-loader"],
},
],
},
};
2
3
4
5
6
7
8
9
10
如果你跟我一样,只打算给 babel-loader 配置 cache 的话,也可以不使用 cache-loader,给 babel-loader 增加选项 cacheDirectory。 cacheDirectory:默认值为 false。当有设置时,指定的目录将用来缓存 loader 的执行结果。之后的 Webpack 构建,将会尝试读取缓存,来避免在每次执行时,可能产生的、高性能消耗的 Babel 重新编译过程。设置空值或者 true 的话,使用默认缓存目录:node_modules/.cache/babel-loader。开启 babel-loader 的缓存和配置 cache-loader,我比对了下,构建时间很接近。
# happypack
当有大量的文件需要解析和处理的时候,wbepack 的构建会变得很慢。HappyPack 可以把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。让 webpack 在同一时刻处理多个任务,从而提升构建速度。
const Happypack = require("happypack");
module.exports = {
//...
module: {
rules: [
{
test: /\.js[x]?$/,
use: "Happypack/loader?id=js",
include: [path.resolve(__dirname, "src")],
},
{
test: /\.css$/,
use: "Happypack/loader?id=css",
include: [
path.resolve(__dirname, "src"),
path.resolve(__dirname, "node_modules", "bootstrap", "dist"),
],
},
],
},
plugins: [
new Happypack({
id: "js", //和rule中的id=js对应
//将之前 rule 中的 loader 在此配置
use: ["babel-loader"], //必须是数组
}),
new Happypack({
id: "css", //和rule中的id=css对应
use: ["style-loader", "css-loader", "postcss-loader"],
}),
],
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32