Webpack-优化篇

avatar

webpack 可优化配置教程

量化

如何直观的查看优化的效果,这时就需要用到 speed-measure-webpack-plugin 插件,该插件可以测量各个插件和loader所花费的时间,使用之后,构建时,会得到类似下面这样的信息:

avatar

使用方法:

1
2
3
4
5
6
7
8
9
//webpack.config.js
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();

const config = {
//...webpack配置
}

module.exports = smp.wrap(config);

exclude/include cache-loader alias noParse extensions resolve

alias

  • 当我们代码中出现 import ‘vue’ 时, webpack 会采用向上递归搜索的方式去node_modules 目录下找。为了减少搜索范围我们可以直接告诉webpack去哪个路径下查找。也就是别名(alias)的配置

exclude/include

  • exclude的优先级要高于include,支持正则或指定路径传入

noParse

  • 如果一些第三方模块没有 AMD/CommonJS 规范版本,可以使用 noParse 来标识这个模块,这样 Webpack 会引入这些模块,但是不进行转化和解析,从而提升 Webpack 的构建性能 ,例如:jquerylodash

extensions

  • webpack会根据extensions定义的后缀查找文件(频率较高的文件类型优先写在前面)

resolve

  • resolve 配置 webpack 如何寻找模块所对应的文件。

avatar

cache-loader

在一些性能开销较大的 loader 之前添加 cache-loader,将结果缓存中磁盘中。默认保存在 node_modueles/.cache/cache-loader 目录下。

首先安装依赖:
npm install cache-loader -D

cache-loader 的配置很简单,放在其他 loader 之前即可。修改Webpack 的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
//...
module: {
//我的项目中,babel-loader耗时比较长,所以我给它配置了`cache-loader`
rules: [
{
test: /\.jsx?$/,
use: ['cache-loader','babel-loader']
}
]
}
}
  • 也可以不使用cache-loader,而给对应 loader 添加 cacheDirectory 配置也能实现缓存

  • cacheDirectory:默认值为 false。当有设置时,指定的目录将用来缓存 loader 的执行结果。之后的 Webpack 构建,将会尝试读取缓存,来避免在每次执行时,可能产生的、高性能消耗的 Babel 重新编译过程。设置空值或者 true 的话,使用默认缓存目录:node_modules/.cache/babel-loader 。开启 babel-loader 的缓存和配置 cache-loader,我比对了下,构建时间很接近。

avatar

happyPack

webpack 构建过程中,实际上耗费时间大多数用在 loader 解析转换以及代码的压缩中。日常开发中我们需要使用 Loader 对js,css,图片,字体等文件做转换操作,并且转换的文件数据量也是非常大。由于js单线程的特性使得这些转换操作不能并发处理文件,而是需要一个个文件进行处理。HappyPack 的基本原理是将这部分任务分解到多个子进程中去并行处理,子进程处理完成后把结果发送到主进程中,从而减少总的构建时间

avatar

thread-loader

除了使用 Happypack 外,我们也可以使用 thread-loader ,把 thread-loader 放置在其它 loader 之前,那么放置在这个 loader 之后的 loader 就会在一个单独的 worker 池中运行。

worker 池(worker pool)中运行的 loader 是受到限制的。例如:

  • 这些 loader 不能产生新的文件。
  • 这些 loader 不能使用定制的 loader API(也就是说,通过插件)。
  • 这些 loader 无法获取 webpack 的选项设置。

安装:npm install thread-loader -D

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
module: {
//我的项目中,babel-loader耗时比较长,所以我给它配置 thread-loader
rules: [
{
test: /\.jsx?$/,
use: ['thread-loader', 'cache-loader', 'babel-loader']
}
]
}
}

thread-loaderHappypack构建时间没什么区别,不过 thread-loader 配置更简单一些。

externals

我们可以将一些JS文件存储在 CDN 上(减少 Webpack 打包出来的 js 体积),在 index.html 中通过 <script> 标签引入,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="root">root</div>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</body>
</html>

我们希望在使用时,仍然可以通过 import 的方式去引用(如 import $ from ‘jquery’ ),并且希望 webpack 不会对其进行打包,此时就可以配置 externals

1
2
3
4
5
6
7
8
//webpack.config.js
module.exports = {
//...
externals: {
//jquery通过script引入之后,全局中即有了 jQuery 变量
'jquery': 'jQuery'
}
}

DLL

DllPluginDLLReferencePlugin 可以实现拆分 bundles,并且可以大大提升构建速度,DllPluginDLLReferencePlugin 都是 webpack 的内置模块。
我们新建一个 webpack 的配置文件,来专门用于编译动态链接库,例如名为: webpack.config.dll.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//webpack.config.dll.js
const webpack = require('webpack');
const path = require('path');

module.exports = {
entry: {
react: ['react', 'react-dom']
},
mode: 'production',
output: {
filename: '[name].dll.[hash:6].js',
path: path.resolve(__dirname, 'dist', 'dll'),
library: '[name]_dll' //暴露给外部使用
//libraryTarget 指定如何暴露内容,缺省时就是 var
},
plugins: [
new webpack.DllPlugin({
//name和library一致
name: '[name]_dll',
path: path.resolve(__dirname, 'dist', 'dll', 'manifest.json') //manifest.json的生成路径
})
]
}

package.jsonscripts 中增加: "build:dll": "webpack --config webpack.config.dll.js"

manifest.json 用于让 DLLReferencePlugin 映射到相关依赖上。
接着更改 webpack.config.js 中的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//webpack.config.js
const webpack = require('webpack');
const path = require('path');
module.exports = {
//...
devServer: {
contentBase: path.resolve(__dirname, 'dist')
},
plugins: [
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, 'dist', 'dll', 'manifest.json')
}),
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: ['**/*', '!dll', '!dll/**'] //不删除dll目录
}),
//...
]
}

使用 npm run build 构建,可以看到 bundle.js 的体积大大减少

修改 index.html 文件,在其中引入 react.dll.js

<script src="/dll/react.dll.9dcd9d.js"></script>

avatar

包的体积

avatar

splitChunks

splitChunks

对于项目越来越大,抽离公共代码就越显得重要,公共代码只需要下载一次就缓存起来了,避免了重复下载。

抽离公共代码对于单页应用和多页应该在配置上没有什么区别,都是配置在 optimization.splitChunks 中。假如我们不配置splitChunks,默认打包出来的是 bundle.js

1
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
//webpack.config.js
module.exports = {
optimization: {
splitChunks: {//分割代码块
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

runtimeChunk 的作用是将包含 chunk 映射关系的列表从 main.js 中抽离出来,在配置了 splitChunk 时,记得配置 runtimeChunk.

1
2
3
4
5
6
7
8
module.exports = {
//...
optimization: {
runtimeChunk: {
name: 'manifest'
}
}
}

最终构建出来的文件中会生成一个 manifest.js

webpack-bundle-analyzer

此时借助 webpack-bundle-analyzer 可以分析项目中哪些包可以拆分
使用方法:

1
2
3
4
5
6
7
8
9
10
11
//webpack.config.prod.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const merge = require('webpack-merge');
const baseWebpackConfig = require('./webpack.config.base');
module.exports = merge(baseWebpackConfig, {
//....
plugins: [
//...
new BundleAnalyzerPlugin(),
]
})

avatar

这时我们可以对 vender 拆分

1
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
module.exports = {
optimization: {
concatenateModules: false,
splitChunks: {//分割代码块
maxInitialRequests:6, //默认是5
cacheGroups: {
vendor: {
//第三方依赖
priority: 1,
name: 'vendor',
test: /node_modules/,
chunks: 'initial',
minSize: 100,
minChunks: 1 //重复引入了几次
},
'lottie-web': {
name: "lottie-web", // 单独将 react-lottie 拆包
priority: 5, // 权重需大于`vendor`
test: /[\/]node_modules[\/]lottie-web[\/]/,
chunks: 'initial',
minSize: 100,
minChunks: 1 //重复引入了几次
},
//...
}
},
},
}

重新构建,结果如下所示:
avatar

结语

参考帖子:

带你深度解锁Webpack系列(优化篇)

2020年了,再不会webpack敲得代码就不香了(近万字实战)