在上一篇文章Webpack配置全解析介绍了Webpack中loader和plugins的一些基本用法,当loader和plugins使用较多后项目也会越来越耗时,因此这次我们继续学习如何优化webpack的配置来让我们的项目运行的更快耗时更短。

  本文将从缩小文件搜索范围、减少打包文件、缓存和多进程四个方面来了解Webpack的优化配置。

缩小文件搜索范围

  Webpack会从Entry入口出发,解析文件中的导入模块语句,再递归解析;每次遇到导入语法时会做两件事情:

  1. 查找导入模块的位置,比如require('vue')就去引入/node_modules/vue/dist/vue.runtime.common.js文件
  2. 通过相应的loader来解析导入的模块,比如引入的js就调用babel-loader来转换代码

  当项目只有几个文件时,解析文件流程只有几百毫秒,然而随着项目规模的增大,解析文件会越来越耗时,因此我们通过webpack的配置来缩小我们搜索模块的范围

优化loader配置

  在上一篇中,我们介绍了使用include/exclude将node_modules中的文件进行包括/排除。

1
2
3
4
5
6
7
8
9
10
{
rules: [{
test: /\.js$/,
use: {
loader: 'babel-loader'
},
// exclude: /node_modules/,
include: [path.resolve(__dirname, 'src')]
}]
}

  include表示哪些目录中的文件需要进行babel-loader,exclude表示哪些目录中的文件不要进行babel-loader。这是因为在引入第三方模块的时候,很多模块已经是打包后的,不需要再被处理,比如vue、jQuery等;如果不设置include/exclude就会被loader处理,增加打包时间。

优化module.noParse配置

  如果一些第三方模块没有使用AMD/CommonJs规范,可以使用noParse来标记这个模块,这样Webpack在导入模块时,就不进行解析和转换,提升Webpack的构建速度;noParse可以接受一个正则表达式或者一个函数:

1
2
3
4
5
6
7
8
{
module: {
//noParse: /jquery|lodash|chartjs/,
noParse: function(content){
return /jquery|lodash|chartjs/.test(content)
}
}
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  对于jQuery、lodash、chartjs等一些库,庞大且没有采用模块化标准,因此我们可以选择不解析他们。

注:被不解析的模块文件中不应该包含requireimport等模块语句

noParse.png

  经过多次打包尝试,打包性能大概能提升10%~20%;本实例完整代码demo

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

优化resolve.modules配置

  modules用于告诉webpack去哪些目录下查找引用的模块,默认值是["node_modules"],意思是在./node_modules查找模块,找不到再去../node_modules,以此类推。

  我们代码中也会有大量的模块被其他模块依赖和引入,由于这些模块位置分布不固定,路径有时候会很长,比如import '../../src/components/button'import '../../src/utils';这时我们可以利用modules进行优化

1
2
3
4
5
6
7
8
9
{
resolve: {
modules: [
path.resolve(__dirname, "src"),
path.resolve(__dirname, "node_modules"),
"node_modules",
],
},
}

  这样我们可以简单的通过import 'components/button'import 'utils'进行导入,webpack会会优先从src目录下进行查找

优化resolve.alias配置

  alias通过创建import或者require的别名,把原来导入模块的路径映射成一个新的导入路径;它和resolve.modules不同的的是,它的作用是用别名代替前面的路径,不是省略;这样的好处就是webpack直接会去对应别名的目录查找模块,减少了搜索时间。

1
2
3
4
5
6
7
{
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
}

  这样我们就能通过import Buttom from '@/Button'来引入组件了;我们不光可以给自己写的模块设置别名,还可以给第三方模块设置别名:

1
2
3
4
5
6
7
{
resolve: {
alias: {
'vue$': isDev ? 'vue/dist/vue.runtime.js' : 'vue/dist/vue.runtime.min.js',
},
},
}

  我们在import Vue from 'vue'时,webpack就会帮我们去vue依赖包的dist文件下面引入对应的文件,减少了搜索package.json的时间。

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

优化resolve.mainFields配置

  mainFields用来告诉webpack使用第三方模块中的哪个字段来导入模块;第三方模块中都会有一个package.json文件用来描述这个模块的一些属性,比如模块名(name)、版本号(version)、作者(auth)等等;其中最重要的就是有多个特殊的字段用来告诉webpack导入文件的位置,有多个字段的原因是因为有些模块可以同时用于多个环境,而每个环境可以使用不同的文件。

  mainFields的默认值和当前webpack配置的target属性有关:

  • 如果target为webworkerweb(默认),mainFields默认值为["browser", "module", "main"]
  • 如果target为其他(包括node),mainFields默认值为["module", "main"]

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  这就是说当我们require('vue')的时候,webpack先去vue下面搜索browser字段,没有找到再去搜索module字段,最后搜索main字段。

  为了减少搜索的步骤,在明确第三方模块入口文件描述字段时,我们可以将这个字段设置尽量少;一般第三方模块都采用main字段,因此我们可以这样配置:

1
2
3
4
5
{
resolve: {
mainFields: ["main"],
}
}

优化resolve.extensions配置

  extensions字段用来在导入模块时,自动带入后缀尝试去匹配对应的文件,它的默认值是:

1
2
3
4
5
{
resolve: {
extensions: ['.js', '.json']
}
}

  也就是说我们在require('./utils')时,Webpack先匹配utils.js,匹配不到再去匹配utils.json,如果还找不到就报错。

  因此extensions数组越长,或者正确后缀的文件越靠后,匹配的次数越多也就越耗时,因此我们可以从以下几点来优化:

  1. extensions数组尽量少,项目中不存在的文件后缀不要列进去
  2. 出现频率比较高的文件后缀优先放到最前面
  3. 在代码中导入文件的时候,要尽量把后缀名带上,避免查找

  以上实例完整代码demo

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

减少打包文件

  在我们项目中不可避免会引入第三方模块,webpack打包时也会将第三方模块作为依赖打包进bundle中,这样就会增加打包文件尺寸和增加耗时,如果能合理得处理这些模块就能提升不少webpack的性能。

提取公共代码

  我们的项目通常有多个页面或者多个页面模块(单页面),多个页面之间通常都有公用的函数或者第三方模块,在每个页面中都打包这些模块会造成以下问题:

  • 资源重复加载,浪费用户流量
  • 每个页面加载资源多,首屏展示慢

  在Webpack4之前,都是通过CommonsChunkPlugin插件来提取公共代码,然而存在着以下问题

  • 产出的chunk在引入的时候,会包含重复的代码
  • 无法优化异步chunk

  Webpack4引入了SplitChunksPlugin插件进行公共模块的抽取;由于webpack4开箱即用的特性,它不用单独安装,通过optimization.splitChunks进行配置即可,官方给的默认配置参数如下:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

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
29
30
31
32
33
34
35
36
37
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
}
}
}
}
};

  我们在home、list、detail三个页面分别引入了vue.js、axios.js和公用的工具函数模块utils.js;我们首先将使用到的第三方模块提取到一个单独的文件,这个文件包含了项目的基础运行环境,一般称为vendors.js;在抽离第三方模块后我们将每个页面都依赖的公共代码提取出来,放到common.js中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = {
optimization: {
splitChunks: {
chunks: 'initial',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: 10,
name: 'vendors'
},
common: {
test: /[\\/]src[\\/]/,
priority: 5,
name: 'common'
}
}
}
}
}

  有时候项目依赖模块比较多,vendors.js文件会特别大,我们还可以对它进一步拆分,按照模块划分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

{
//省略其他配置
cacheGroups: {
//涉及vue的模块
vue: {
test: /[\\/]node_modules[\\/](vue|vuex|vue-router)/,
priority: 10,
name: 'vue'
},
//其他模块
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: 9,
name: 'vendors'
},
common: {
test: /[\\/]src[\\/]/,
priority: 5,
name: 'common'
}
}
}

动态链接DllPlugin

  DLL即动态链接库(Dynamic-Link Library)的缩写,熟悉Windows系统的童鞋在电脑中也经常能看到后缀是dll的文件,偶尔电脑弹框警告也是因为电脑中缺失了某些dll文件;DLL最初用于节约应用程序所需的磁盘和内存空间,当多个程序使用同一个函数库时,DLL可以减少在磁盘和内存中加载代码的重复量,有助于代码的复用。

  在Webpack中也引入了DLL的思想,把我们用到的模块抽离出来,打包到单独的动态链接库中去,一个动态链接库中可以有多个模块;当我们在多个页面中用到某一个模块时,不再重复打包,而是直接去引入动态链接库中的模块。

  Webpack中集成了对动态链接库的支持,主要用到的两个插件:

  • DllPlugin:创建动态链接库文件
  • DllReferencePlugin:在主配置中引入打包好的动态链接库文件

  我们首先使用DllPlugin来创建动态链接库文件,在项目下新建webpack.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
const path = require("path");
const webpack = require("webpack");

module.exports = {
mode: "production",
entry: {
vue: ["vue", "vuex", "vue-router"],
vendor: ["dayjs", "axios", "mint-ui"],
},
output: {
path: path.resolve(__dirname, "public/vendor"),
// 指定文件名
filename: "[name].dll.js",
//暴露全局变量的名称
library: "[name]_dll_lib",
},
plugins: [
new webpack.DllPlugin({
path: path.join(__dirname, "public", "vendor", "[name].manifest.json"),
name: "[name]_dll_lib",
}),
],
};

  这里entry设置了多个入口,每个入口也有多个模块文件;然后在package.json添加打包命令

1
2
3
4
5
{
"scripts":{
"build:dll": "webpack --config=webpack.dll.js"
}
}

  执行npm run build:dll后,我们在/public/vendor目录下得到了我们打包后的动态链接库的文件:

1
2
3
4
├── vendor.dll.js
├── vendor.manifest.json
├── vue.dll.js
└── vue.manifest.json

  生成出来的打包文件正好是以两个入口名来命名的,以vue为例,看一下vue.dll.js的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var vue_dll_lib =
/******/ (function(modules) {
// 省略webpackBootstrap代码
/******/ })
/******/ ({

/***/ "./node_modules/vue-router/dist/vue-router.esm.js":
/***/ (function(module, exports, __webpack_require__) {
//省略vue-router模块代码
/***/ }),

/***/ "./node_modules/vue/dist/vue.runtime.esm.js":
/***/ (function(module, exports, __webpack_require__) {
//省略vue模块代码
/***/ }),

/***/ "./node_modules/vuex/dist/vuex.esm.js":
/***/ (function(module, exports, __webpack_require__) {
//省略vuex模块代码
/***/ }),

/******/ });

  可以看出,动态链接库中包含了引入模块的所有代码,这些代码存在一个对象中,通过模块路径作为键名来进行引用;并且通过vue_dll_lib暴露到全局;vue.manifest.json则是用来描述动态链接库文件中包含了哪些模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"name": "vue_dll_lib",
"content": {
"./node_modules/vue-router/dist/vue-router.esm.js": {
"id": "./node_modules/vue-router/dist/vue-router.esm.js",
"buildMeta": {}
},
"./node_modules/vue/dist/vue.runtime.esm.js": {
"id": "./node_modules/vue/dist/vue.runtime.esm.js",
"buildMeta": {}
},
"./node_modules/vuex/dist/vuex.esm.js": {
"id": "./node_modules/vuex/dist/vuex.esm.js",
"buildMeta": {}
},
}
}

  manifest.json描述了对应js文件包含哪些模块,以及对应模块的键名(id),这样我们在模板页面中就可以将动态链接库作为外链引入,当Webpack解析到对应模块时就通过全局变量来获取模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<!-- 引入动态链接库 -->
<script src="./vendor/vendor.dll.js"></script>
<script src="./vendor/vue.dll.js"></script>
</body>
</html>

  最后我们在打包时,通过DllReferencePlugin将动态链接库引入到主配置中:

1
2
3
4
5
6
7
8
9
10
11
12
13
//webpack.config.js
{
plugins: [
new webpack.DllReferencePlugin({
context: path.join(__dirname),
manifest: require('./public/vendor/vendor.manifest.json')
}),
new webpack.DllReferencePlugin({
context: path.join(__dirname),
manifest: require('./public/vendor/vue.manifest.json')
}),
]
}

注:动态链接库打包到/public/vendor目录下,还需要通过CopyWebpackPlugin插件将它拷贝到生成后的目录中,否则会出现引用失败的报错;打包动态链接库文件只需要执行一次,除非以后模块升级或者引入新的模块。

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  引入动态链接库可以将项目中一些不经常更新的模块放到外部文件中,我们再次打包页面逻辑代码时会发现构建速度有了比较大的提升,大概30%~40%,相关代码在demo10

externals

  我们在项目打包时,有一些第三方的库会从CDN引入(比如jQuery等),如果在bundle中再次打包项目就过于臃肿,我们就可以通过配置externals将这些库在打包的时候排除在外。

1
2
3
4
5
6
7
8
{
externals: {
'jquery': "jQuery",
'react': 'React',
'react-dom': 'ReactDOM',
'vue': 'Vue'
}
}

  这样就表示当我们遇到require('jquery')时,从全局变量去引用jQuery,其他几个包也同理;这样打包时就把jquery、react、vue和react-dom从bundle中剔除了,本实例完整代码demo

Tree Shaking

  Tree Shaking最早由rollup实现,后来webpack2页实现了这项功能;Tree Shaking的字面意思是摇树,一棵树上有一些树叶虽然还挂着,但是它可能已经死掉了,通过摇树方式把这些死掉的树叶去除。

tree_shake.gif

  我们项目中也是同样的,我们并没有用到文件的所有模块,但是webpack仍会将整个文件打包进来,文件中一直用不到的代码就是“死代码”;这种情况就用用到Tree Shaking帮我们剔除这些用不到的代码模块。

  比如我们定义了一个utils.js文件导出了很多工具模块,然后在index.js中只引用了某些模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//utils.js
var toString = Object.prototype.toString;

export function isArray(val) {
return toString.call(val) === '[object Array]';
}
export function isFunction(val) {
return toString.call(val) === '[object Function]';
}
export function isDate(val) {
return toString.call(val) === '[object Date]';
}
//index.js
import { isArray } from './utils'
isArray()

  我们希望在代码中只打包isArray函数到bundle中;需要注意的是,为了让Tree Shaking生效,我们需要使用ES6模块化的语法,因为ES6模块语法是静态化加载模块,它有以下特点:

  1. 静态加载模块,效率比CommonJS 模块的加载方式高
  2. ES6 模块是编译时加载,使得静态分析成为可能进一步拓宽JS的语法

  如果是require,在运行时确定模块,那么将无法去分析模块是否可用,只有在编译时分析,才不会影响运行时的状态。

  使用ES6模块后还有一个问题,因为我们的代码一般都采用babel进行编译,而babel的preset默认会将任何模块类型编译成Commonjs,因此我们还需要修改.babelrc配置文件:

1
2
3
4
5
6
7
8
9
10
11
{
"presets": [
[
"@babel/preset-env",
{
//添加modules:false
"modules": false
}
]
]
}

  配置好babel后我们需要让webpack先将“死代码”标识出来:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
2
3
4
5
6
7
{
//其他配置
optimization: {
usedExports: true,
sideEffects: true,
}
}

  运行打包命令后,当我们打开输出的bundle文件时,我们发现虽然一些“死代码”还存在里面,但是加上了一个unused harmony export的标识

1
2
3
4
5
6
7
8
9
/* unused harmony export isFunction */
/* unused harmony export isDate */
var toString = Object.prototype.toString;
function isFunction(val) {
return toString.call(val) === '[object Function]';
}
function isDate(val) {
return toString.call(val) === '[object Date]';
}

  虽然webpack给我们指出了哪些函数用不上,但是还需要我们通过插件来剔除;由于uglifyjs-webpack-plugin不支持ES6语法,这里我们使用terser-webpack-plugin的插件来代替它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const TerserJSPlugin = require("terser-webpack-plugin");
module.exports = {
optimization: {
usedExports: true,
sideEffects: true,
minimize: true,
minimizer: [
new TerserJSPlugin({
cache: true,
parallel: true,
sourceMap: false,
}),
],
}
}

  这样我们发现打包出来的文件就没有多余的代码了。

注: Tree Shaking在生产环境(production)是默认开启的

  对于我们常用的一些第三方模块,我们也可以实现Tree Shaking;以lodash为例,它整个包有非常多的函数,但并不是所有的函数都是我们所用到的,因此我们也需要对它没有用到的代码进行剔除。

1
2
3
//index.js
import { chunk } from 'lodash'
console.log(chunk([1,2,3,4], 2))

  打包出来发现包的大小还是能达到70+kb,如果只引用了chunk不应该有这么大;我们打开/node_modules/lodash/index.js发现他还是使用了require的模式导入导出模块,因此导致Tree Shaking失败;我们先安装使用ES6模块版本的lodash:npm i -S lodash-es,然后修改引入包:

1
2
3
//index.js
import { chunk } from 'lodash-es'
console.log(chunk([1,2,3,4], 2))

  这样我们生成的bundle包就小很多;本实例完整代码demo

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

缓存

  我们知道webpack会对不同的文件调用不同的loader进行解析处理,解析的过程也是最耗性能的过程;我们每次改代码也只是修改项目中的少数文件,项目中的大部分文件改动的次数不是那么频繁;那么如果我们将解析文件的结果缓存下来,下次发现同样的文件只需要读取缓存就能极大的提升解析的性能。

cache-loader

  cache-loader可以将一些对性能消耗比较大的loader生产的结果缓存在磁盘中,等下次再次打包时如果是相同的代码就可以直接读取缓存,减少性能消耗。

注:保存和读取缓存也会产生额外的性能开销,因此cache-loader适合用于对性能消耗较大的loader,否则反而会增加性能消耗

  cache-loader的使用也非常简单,安装后在所需要缓存的loader前面添加即可(因为loader加载的顺序是反向的),比如我们需要给babel-loader添加缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
//省略其他代码
rules: [
{
test: /\.js/,
use: [
{
loader: 'cache-loader'
},
{
loader: "babel-loader",
},
],
},
],
}

  然而我们发现第一次打包的速度并没有发生明显变化,甚至可能还比原来打包的更慢了;同时还多了/node_modules/.cache/cache-loader/这个目录,看名字就是一个缓存文件;我们继续打包,下面图表记录了我几次打包的耗时:

cache-loader-time.png

  我们发现第一次打包时间都差不多,但是第二次开始缓存文件就开始发挥了重要的作用了,直接减少了75%的耗时。

  除了使用cache-loader,babel-loader也提供缓存功能,通过cacheDirectory进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
rules: [
{
test: /\.js/,
use: [
{
loader: "babel-loader",
options: {
cacheDirectory: true
}
},
],
},
],
}

  在/node_modules/.cache/babel-loader也多了缓存文件。经过两个使用结果的对比,cache-loader的性能提升更加出色一些;本实例完整代码demo

HardSourceWebpackPlugin

  HardSourceWebpackPlugin也可以为模块提供缓存功能,同意也是将文件缓存在磁盘中

  首先通过npm i -D hard-source-webpack-plugin来安装插件,并且在配置中添加插件:

1
2
3
4
5
6
7
var HardSourceWebpackPlugin = 
require('hard-source-webpack-plugin');
module.exports = {
plugins: [
new HardSourceWebpackPlugin()
]
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  一般HardSourceWebpackPlugin默认缓存是在/node_modules/.cache/hard-source/[hash]目录下,我们可以设置它的缓存目录和何时创建新的缓存哈希值。

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
29
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
},
})
]
}

hard-source.png

  通过尝试多次打包,发现能节省大概90%的时间;本实例完整代码demo

多进程

  我们在事件循环中讲到过,js是一门单线程的语言,在同一事件线上只有一个线程在处理任务;因此在webpack解析到JS、CSS、图片或者字体文件时,它需要一个个的去解析编译,不能同时处理多个任务;我们可以通过插件来将任务分给多个子进程去并发执行,子进程处理完成后再将结果发送给主进程。

happypack

  happypack会自动帮我们分解任务和管理进程,通过名字我们也能看出来,这是一款能够带来快乐的插件。

enough.gif

  我们通过npm i -D happypack后就能在webpack中进行配置了:

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
const happypack = require("happypack");
module.exports = {
module: {
rules: [
{
test: /\.js/,
exclude: /node_modules/,
//将js文件处理给id为js的happypack实例
use: "happypack/loader?id=js",
}
],
},
plugins: [
//通过id标识当前happypack是处理什么文件的
new happypack({
id: "js",
//调用处理文件的loader,用法和rules中一致
loaders: [{
loader: "babel-loader",
},
{
loader: "eslint-loader",
},
],
}),
],
}

  我们将rules/loader的处理全部交给了happypack进行处理,并且通过id来调用具体的实例,然后在实例中配置具体的loader进行处理;在happypack的实例中除了id和loaders我们还可以配置进程数量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//共享进程池,进程池中包含5个子进程
var happyThreadPool = happypack.ThreadPool({
size: 5
});
{
plugins: [
new happypack({
id: "js",
//开启几个子进程,默认3个
threads: 3,
//共享进程池
threadPool: happyThreadPool,
//是否允许 HappyPack 输出日志
verbose: true,
loaders: [{
loader: "babel-loader",
},
{
loader: "eslint-loader",
},
],
}),
],
}

注:threads和threadPool字段只需要配置一个即可。

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  我们通过happypack.ThreadPool创建了一个包含5个子进程的共享进程池,每个happypack实例可以通过共享进程池来处理文件;相对于给每个happypack实例分配进程,这样可以防止占用过多无用的进程;我们打包看一下所耗时间:

happypack.png

  我们发现有了happypack耗时居然还增加了20%~30%,说好的多进程带来快乐呢。

catch_up.png

  由于我们的项目不够庞大,而加载多进程也需要耗费时间和性能,因此我们才会出现使用了happypack反而增加耗时的情况;所以一般happypack适用于比较大的项目中;本实例完整代码demo

thread-loader

  把thread-loader放置在其他loader之前,在它之后的loader就会在一个单独的进程池中运行,但是在进程池中运行的loader有以下限制:

  因此,也就是说像MiniCssExtractPlugin.loader等一些提取css的loader是不能使用thread-loader的;跟happypack一样,它也只适合用于文件较多的大项目:

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
"thread-loader",
"babel-loader"
]
}
]
}
}

  本实例完整代码demo

更多前端资料请关注作者公众号``【前端壹读】``。
PS:公众号接入了图灵机器人小壹,欢迎各位老铁来撩。

本文地址: http://xieyufei.com/2020/07/30/Webpack-Optimize.html