# Webpack 配置全解析(优化篇)
# 前言
在上一篇文章《Webpack 配置全解析》介绍了 webpack 中的 loader 和 plugins 的一些基本用法,当 loader 和 plugins 使用较多后项目也会变得越来越卡,因此我们继续来学习如何优化 webpack 的配置让我们的项目运行得更快,耗时更短。
本文将从缩小搜索范围、减少打包文件、缓存和多进程四个方面来了解 Webpack 的优化配置。
# 缩小文件搜索范围
Webpack 会从 Entry 入口出发,解析文件中的导入模块语句,再递归解析;每次遇到导入语法时会做两件事情:
- 查找导入模块的位置
- 通过相应的 loader 来解析导入的模块
当项目只有几个文件时,解析文件流程只有几百毫秒,然而随着项目规模的增大,解析文件会越来越耗时,因此我们通过 webpack 的配置来缩小我们搜索模块的范围。
# 优化 loader 配置
上文中我们介绍了 include / exclude 将 node_modules 中的文件进行包括 / 排除。
{
rules: [{
test: /\.js$/,
use: ['babel-loader'],
// exclude: /node_modules/,
include: path.resolve(__dirname, 'src')
}]
}
include 表示哪些目录中的文件需要进行 babel-loader,exclude 表示哪些目录中的文件不要进行 babel-loader。很多第三方依赖都已经进行过转换,其实无需再次转换,否则会耗费很多时间。
# 优化 module.noParse 配置
如果一些第三方模块没有使用 AMD/CommonJS规范,可以使用 noParse 来标记这个模块,这样 Webpack 在导入模块时,就不进行解析和转换,提升 Webpack 的构建速度;noParse 可以接受一个正则表达式或者是一个函数:
{
module: {
// noParse: /jquery|lodash|chartjs/
noParse(content) {
return /jquery|lodash|chartjs/.test(content)
}
}
}
驻:不被解析的文件中不应该包含 require、import 等模块语句
# 优化 resolve.modules 配置
modules 用于告诉 webpack 去哪些目录下查找引用模块,默认值是 ["node_modules"]
,意思是在 ./node_modules
查找模块,找不到再去 ../node_modules
,以此类推。
我们代码中也会有大量的模块被其他模块依赖引入,由于这些模块位置分布不固定,路径有时候会很长,这是我们可以利用 modules 进行优化。
# 优化 resolve.alias 配置
alias 通过创建 import 或者 require 的别名,把原来导入模块的路径映射成一个新的导入路径;它和 resolve.modules
不同的是,它的作用是用别名代替前面的路径,不是省略;这样的好处就是 webpack 直接回去对应别名的目录查找模块,减少搜索时间。
我们不光可以给自己写的模块设置别名,还可以给第三方模块设置别名。
# 优化 resolve.mainFields 配置
mainFields 用来告诉 webpack 使用第三方模块中的哪个字段来导入模块;第三方模块中都会有一个 package.json 文件用来描述这个模块的一些属性,比如模块名(name)、版本号(version)、作者(auth)等等;其中最重要的就是有多个特殊的字段用来告诉 webpack 导入文件的位置,有多个字段的原因是因为有些模块可以同时用于多个环境,而每个环境可以使用不同的文件。
mainFields 的默认值和当前 webpack 配置的 target 属性有关:
- 如果 target 为 webworker 或 web(默认),mainFields 默认值为 ["browser", "module", "main"]
- 如果 target 为其他(包括node),mainFields 默认值为 ["module", "main"]
这就是说当我们 require('vue')
的时候,webpack 先去 vue 下面搜索 browser 字段,没有找到再去搜索 module 字段,最后搜索main字段。
为了减少搜索的步骤,在明确第三方模块入口文件描述字段时,我们可以将这个字段设置尽量少;一般第三方模块都采用 main 字段,因此我们可以这样配置:
{
resolve: {
mainFields: ["main"],
}
}
# 优化 resolve.extensions 配置
extensions 字段用来在导入模块时,自动带上后缀尝试去匹配对应的文件,它的默认值是 ['.js', '.json']
。
因此,extensions 数组越长,或者正确后缀的文件越靠后,匹配的次数越多就越耗时,因此我们可以从以下几点来优化:
- extensions 数组尽量少,项目中不存在的文件后缀不要列进去;
- 出现频率比较高的文件后缀名优先放到最前面;
- 在代码导入时,要尽量把后缀名带上,避免查找。
# 减少打包文件
项目中不可避免会引入第三方模块,webpack 打包时也会将第三方模块作为依赖打包进 bundle 中,这样就会增加打包文件尺寸和增加耗时,如果能合理处理这些模块就能提升不少 webpack 的性能。
# 提取公共代码
我们项目通常有多个页面或者多个模块,多个页面之间通常会有公共函数,每个页面都打包这些模块会造成以下问题:
- 资源重复加载,浪费用户流量;
- 每个页面加载资源多,首屏展示慢。
webpack4 引入了 SplitChunksPlugin 插件进行公共模块的抽取,开箱即用,通过 optimization.splitChunks
进行配置即可,默认配置如下:
module.exports = {
optimization: {
splitChunks: {
// 代码分割时默认对异步代码生效,all:所有代码有效,inital:同步代码有效
chunks: 'async',
// 代码分割最小的模块大小,引入的模块大于 20000B 才做代码分割
minSize: 20000,
// 代码分割最大的模块大小,大于这个值要进行代码分割,一般使用默认值
maxSize: 0,
// 引入的次数大于等于1时才进行代码分割
minChunks: 1,
// 最大的异步请求数量,也就是同时加载的模块最大模块数量
maxAsyncRequests: 30,
// 入口文件做代码分割最多分成 30 个 js 文件
maxInitialRequests: 30,
// 文件生成时的连接符
automaticNameDelimiter: '~',
enforceSizeThreshold: 5000,
cacheGroups: {
vendors: {
// 位于node_modules中的模块做代码分割
test: /[\\/]node_modules[\\/]/,
// 根据优先级决定打包到哪个组里,例如一个 node_modules 中的模块进行代码
priority: -10
},
// 既满足 vendors,又满足 default,那么根据优先级会打包到 vendors 组中。
default: {
// 没有 test 表明所有的模块都能进入 default 组,但是注意它的优先级较低。
// 根据优先级决定打包到哪个组里,打包到优先级高的组里。
priority: -20,
//如果一个模块已经被打包过了,那么再打包时就忽略这个上模块
reuseExistingChunk: true
}
}
}
}
};
# externals
我们在项目打包时,项目中引用的一些第三方依赖通常都是不会发生变动的,因此放到 CDN 更加合适,因此就需要排除掉这些使用 CDN 的依赖。
{
externals: {
'jquery': 'jQuery',
'react': 'React',
'react-dom': 'ReactDOM',
'vue': 'Vue'
}
}
这就表示当我们遇到 require('jquery')
时,会从全局变量去引用 jQuery,其他也同理。这样打包就会把这些依赖从 bundle 中剔除。
# Tree Shaking
具体实现原理不介绍
# 缓存
我们知道 webpack 会对不同的文件调用不同的 loader 进行解析处理,解析的过程也是最耗性能的过程;我们每次改代码也只是修改项目中的少数文件,项目中的大部分文件改动的次数不是那么频繁;那么如果我们将解析文件的结果缓存下来,下次发现同样的文件只需要读取缓存就能极大的提升解析的性能。
# cache-loader
使用方式很简单,只需要在需要 cache 的 loader 前面添加 cache-loader 即可。
但是 cache-loader 本身也会耗费性能,因此只适合对性能消耗较大的 loader。
# HardSourceWebpackPlugin
这个插件也可以提供缓存功能,也是安装在磁盘中。
一般HardSourceWebpackPlugin默认缓存是在 /node_modules/.cache/hard-source/[hash]
目录下,我们可以设置它的缓存目录和何时创建新的缓存哈希值。
module.exports = {
plugins: [
new HardSourceWebpackPlugin({
//设置缓存目录的路径
//相对路径或者绝对路径
cacheDirectory: 'node_modules/.cache/hard-source/[confighash]',
//构建不同的缓存目录名称
//也就是cacheDirectory中的[confighash]值
configHash: function(webpackConfig) {
return require('node-object-hash')({sort: false}).hash(webpackConfig);
},
//环境hash
//当loader、plugin或者其他npm依赖改变时进行替换缓存
environmentHash: {
root: process.cwd(),
directories: [],
files: ['package-lock.json', 'yarn.lock'],
},
//自动清除缓存
cachePrune: {
//缓存最长时间(默认2天)
maxAge: 2 * 24 * 60 * 60 * 1000,
//所有的缓存大小超过size值将会被清除
//默认50MB
sizeThreshold: 50 * 1024 * 1024
},
})
]
}
# 多进程
由于 js 是一门单线程语言,同时时间只能处理一个任务,因此 webpack 解析对应的文件类型时需要一个个依次去解析。我们可以通过将任务分发给多个子线程并发执行任务,从而提高解析时间。
# happypack
作者已经不维护了。
# thread-loader
把thread-loader放置在其他loader之前,在它之后的loader就会在一个单独的进程池中运行,但是在进程池中运行的loader有以下限制:
- 这些 loader 不能产生新的文件
- 这些 loader 不能使用定制的 loader API(也就是说,通过插件)
- 这些 loader 无法获取 webpack 的选项设置
因此,也就是说像 MiniCssExtractPlugin.loader 等一些提取 css 的 loader 是不能使用 thread-loader 的。