记住4点优化就可提高webpack构建速度
前言
一、缩小文件的搜索范围
这点主要在webpack的配置上做些优化,这边记录了6个优化小点:
1、优化 Loader 配置
可以通过test、include、exclude三个配置项来命中Loader要应用规则的文件。为了尽可能少地让文件被 Loader 处理,可以通过 include 去命中只有哪些文件需要被处理。
示例如下:
// 配置es6 babel-loader
module.exports = {
module: {
rules: [
{
// 如果项目中只有js就不用写/\.jsx?$/
test: /\.js$/,
// 排除 node_modules文件夹下文件
exclude: /node_modules/,
// 只编译项目根目录src目录下js文件
include: [
path.resolve(__dirname, "../src"),
],
loader: "babel-loader"
}
]
},
};
// 注意:exclude优先级比include高,通常配置include后则无需再配置exclude,除非需要排除include中的某些文件
2、优化 resolve.modules 配置
resolve modules 的默认值是['node_modules'],含义是先去当前目录的 node_modules目录下去找我们想找的模块,如果没找到就去上一级目录. /node_modules中找,再没有就去 ../../node_modules 中找,以此类推。
因此当第三方模块都放在项目根目录的./node_modules目录下时,就没有必要按照默认的方式去一层层地寻找,可以指明存放第三方模块的绝对路径,以减少寻找,配置如下:
module.exports = {
resolve: {
// 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
modules: [path.resolve(__dirname, 'node_modules')]
}
}
3、优化 resolve.mainFields 配置
在安装的第三方模块中都会有一个package.json 文件,用于描述这个模块的属性,其中的某些字段用于描述入口文件在哪里。resolve.mainFields 用于配置采用哪个字段作为入口文件的描述。
如:fetch 插件,需要支持用于浏览器和 Node.js 环境,因此在它的 package. jsoη中就有两个入口文件描述字段:
{
browser : "fetch-npm-browserify.js",
main: "fetch-npm-node.js"
}
因此我们需要根据当前webpack.target配置来指明要引用哪个文件。
示例:
module.exports = {
resolve: {
// 只采用 main 字段作为入口文件的描述字段,以减少搜索步骤
mainFields: ['main']
}
}
4、优化 resolve.alias 配置
通过别名来将原导入路径映射成一个新的导入路径。以react包为例,在默认情况下, Webpack 会从入口文件./node_modules/react/react.js 开始递归解析和处理依赖的几十个文件,这会是一个很耗时的操作。通过配置resolve.alias可以让 Webpack 在处理React库时,直接使用单独、完整的 react.min.js 文件 ,从而跳过耗时的递归解析操作。
示例:
module.exports = {
resolve: {
// 使用alias将导入react的语句换成直接使用单独、完整的react.min.js文件
// 减少耗时的递归解析操作
alias: path.resolve(__dirname ,'./ node_modules/react/dist/react.min.js')
}
}
5、优化 resolve.extensions 配置
在导入语句没带文件后缀时Webpack会在自动带上后缀后去尝试询问文件是否存在。默认是['.js','.json']。在项目中如果明确知道只需匹配'.js'或'.jsx'后缀的话,则可以指定匹配后缀列表以减少匹配次数。
示例:
module.exports = {
resolve: {
// 后缀尝试列表要尽可能小
// 频率出现最高的文件后缀要优先放在最前面
// 在源码中写导入语句时,要尽可能带上后缀
extensions: ['.js']
}
}
6、优化 resolve.noParse 配置
忽略对部分没采用模块 化的文件的递归解析处理,这样做的好处是能提高构建性能。原因是一些库如 jQuery。在前面讲解优化 resolve.alias 配置时讲到,单独、完整的react.min.js文件没有采用模块化,这时我们就需要通过配置 module.noParse忽略对 react.main.js 文件的递归解析处理。
示例:
module.exports = {
resolve: {
// 忽略对 react.min.js文件的递归解析处理
noParse: [/react\.min\.js$/]
}
}
二、使用 DllPlugin
从插件名字可以看出这是个借助DLL思想来优化webpack构建速度。在window中DLL后缀文件表示动态链接库文件,其中包含为其他模块调用的函数和数据。
为什么为 Web 项目构建接入动态链接库的思想后会大大提升构建速度呢?原因在于,包含大量复用模块的动态链接库只需被编译一次,在之后的构建过程中被动态链接库包含的模块将不会重新编译,而是直接使用动态链接库中 的代码。从这里我们也可以看出动态链接库抽离的代码通常是第三方库代码,如react、react-dom、vue等,只有这些库不升级那动态链接库就不需要重新编译。
Webpack 己经内置了对动态链接库的支持,需要通过以下两个内置的插件接入:
1、DllPlugin 插件:用于打包出一个个单独的动态链接库文件
2、DllReferencePlugin 插件:在主要的配置文件中引入 DllPlugin 插件打包好的动态链接库文件。
我们以打包react动态链接库为列,首先我们需要打包出react.dll.js、manifest.json。其中react.dll.js包含react、react-dom模块代码,而manifest.json则是描述react.dll.js中包含了哪些模块。在webpack构建过程中遇到react模块时首先会从manifest.json中寻找是否有该模块,如果有则直接从react.dll.js中引用该模块跳过对react的编译。生成react.dll.js、manifest.json我们需要用到DllPlugin插件,我们需要单独配置一个webpack.dll.config.js文件用来生成这个两个文件,内容如下:
const path = require('path');
// webpack 内置插件,无需install
const DllPlugin = require('webpack/lib/DllPlugin');
module.exports = {
// JavaScript 执行入口文件
mode: 'production',
entry: {
// 将 React 相关的模块放到一个单独的动态链接库中
react: ['react', 'react-dom'],
},
output: {
// 输出的动态链接库的文件名称,[name]代表当前动态链接库的名称,
// 也就是entry中配置的 react
filename: '[name].dll.js',
// 将输出的文件都放到 dist 目录下
path: path.resolve(__dirname, '../public/dll'),
// 存放动态链接库的全局变量名称,例如对于 react 来说就是 _dll_react
// 之所以在前面加上_dll_ ,是为了防止全局变量冲突
library: '_dll_[name]',
},
plugins: [
// 接入 DllPlugin
new DllPlugin({
// 动态链接库的全局变量名称,需要和 output.library 中的保持一致
// 该字段的值也就是输出的 manifest.json 文件中 name 字段的值
// 例如在 react.manifest.json 中就有 "name":"_dll_react"
name: '_dll_[name]',
// 描述动态链接库的 manifest.json 文件输出时的文件名称
path: path.join(__dirname, '../public/dll', '[name].manifest.json'),
})
]
};
在package.json中配置dll命令,如下:
{
"scripts": {
"dll": "webpack --config ./config/webpack.dll.config.js"
}
}
这样我们在命令版中执行yarn dll 就生成了react.dll.js、manifest.json这两个文件。
由于我们暴露出来的模块是通过_dll_react这个全局变量来引用其所包含的所有模块,因此我们需要在index.html模板中加载该文件,如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<title><%= htmlWebpackPlugin.options.title %></title>
<script ></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
最后我们在项目的webpack中使用这个动态链接库文件,这里就用到了DllReferencePlugin 插件,配置如下:
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');
module.exports = {
// 省略其他配置项
plugins: [
// 告诉Webpack使用了哪些动态链接库
new DllReferencePlugin({
// 描述 react 动态链接库的文件内容
manifest: path.resolve(__dirname, '../public/dll/react.manifest.json'),
}),
// 资源拷贝,将生成在puplic下的dll文件拷贝到dist目录下
new CopyWebpackPlugin([
{
from: path.resolve(__dirname + '/../public'),
to: path.resolve(__dirname + '/../dist'),
ignore: ['*.jpg']
}
])
],
};
至此,我们项目就成功引入了DllPlugin动态链接库的构建方案。
三、使用 HappyPack
webpack打包哪一步最耗时?可能要数loader对文件的转换操作了,使用loader将文件转换为我们需要的类型,文件数量巨大,webpack执行又是单线程的,转换的操作只能一个一个的处理,不能多件事一起做。我们需要Webpack 能同一时间处理多个任务,发挥多核 CPU 电脑的威力,HappyPack 就能让 Webpack 做到这点,我们将需要通过loader处理的文件先交给happypack去处理,happypack 在收集到这些文件的处理权限后,统一分配CPU资源,从而发挥多核作用。
直接看实例:
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HappyPack = require('happypack')
//构造出一个共享进程池,在进程池中包含4个子进程
const happyThreadPool = HappyPack.ThreadPool({
size: 4
})
module.exports = {
//省略部分配置
module: {
rules: [
{
test: /\.js$/,
//将对.js文件的处理转交给id为babel的happypack实例
use: ['happypack/loader?id=babel']
//排除node_modules下的js,合理的使用排除可以事半功倍
exclued:path.resolve(__dirname,'node_modules')
},
{
test: /\.css$/,
//将对.css文件的处理转交给id为css的happypack实例
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: ['happypack/loader?id=css']
})
}]
},
plugins: [
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// 如何处理 .js 文件,用法和 Loader 配置中一样
loaders: ['babel-loader?cacheDirectory=true'],
//使用共享进程池中的自进程去处理任务
threadPool: happyThreadPool,
//是否允许happypack输出日志,默认true
verbose: true
}),
new HappyPack({
id: 'css',
// 如何处理 .css 文件,用法和 Loader 配置中一样
loaders: ['css-loader!postcss-loader'],
threadPool: happyThreadPool
}),
]
}
在以上代码中有以下两项重要的修改。
在Loader 置中,对所有文件的处理都交给了 happypack/loader ,使用紧跟 其后的querystring?id=babel去告诉happypack/loader 选择哪个HappPack实例处理文件。
在Plugin 配置中新增了两个 HappyPack 实例,分别用于告诉 happypack /loader 如何处理.js和.css 文件。选项中的 id 属性的值和上面 querystring 中的?id=babe 对应,选项中的 loaders 属性和 Loader 配置中的一样
接入 HappyPack 后,需要为项目安装新的依赖:
npm i -D happypack
四、使用 ParallelUglifyPlugin
这个原理跟HappyPack有点类似,都是将单线程分解成多个子进程去处理文件。在发布线上代码时,通常需要对代码进行压缩,由于压缩 JavaScript 代码时,需要先将代码解析成用 Object 抽象表示的 AST 语法树, 再去应用各种规则分析和处理 AST ,所以导致这个过程的计算量巨大耗时非常多。
当Webpack 有多个 JavaScript 文件需要输出和压缩时原本会使用 UglifyJS 去一个一个压缩 再输出,但是 ParallelUglifyPlugin 会开启多个子进程,将对多个文件的压缩工作分配给多个 子进程去完成,每个子进程其实还是通过 UglifyJS 去压缩代码,但是变成了并行执行。因此可以更快完成压缩工作。
使用示例如下:
const path = require('path');
const ParallelUglifyPlugin =require('webpack-parallel-uglify-plugin');
module.exports = {
// 忽略其他配置项
plugins: [
new ParallelUglifyPlugin({
// 传递给UglifyJS 的参数
uglifyJS: {
output: {
// 最紧凑输出
beautify: false,
// 删除注释
comments: false
},
compress: {
// 删除console语句
drop_console: true,
collapse_vars: true,
reduce_vars: true
}
}
})
]
};
在通过 new ParallelUglifyPlugin() 实例化时,支持以下参数。
test: 使用正则去匹配哪些文件需要被 ParallelUglifyPlugin 压缩
include: 使用正则去命中需要被 ParallelU glifyPlugin 压缩的文件
exclude :使用正则去命中不需要被 ParallelUglifyPlugin 压缩的文件
cacheDir :缓存压缩后的结果
workerCount :开启几个子进程去并发执行压缩
sourceMap :是否输出 Source Map ,这会导致压缩过程变慢
uglifyJS :用于压缩 ES5 代码时的配置
其中的 test include exclude 与配置 Loader 时的思想和用法一样
以上就是优化webpack构建速度的四点建议,如果你还在为项目构建速度缓慢而烦恼,不如试试上诉优化万一变好了呢。
参考文献
《深入浅出 Webpack》 吴浩麟
前端扫盲
扫码关注