webpack构建优化小记

  webpack相信大家都不陌生了,在工作当中相信大家会经常用到,在处理资源构建打包的时候非常的有用,本文分享一下我在使用webpack进行项目构建的过程中总结的一些优化经验。

模块的查找

当我们在项目中使用到第三方模块的时候,webpack在构建时,会先去当前目录下的 ./node_modules 目录下去找想找的模块,如果没找到就去上一级目录 ../node_modules 中找,再没有就去../../node_modules 中找,以此类推,到这里,是不是觉得和nodejs里面模块的寻找机制很相似

当我们在安装第三方模块时,默认会安装在当前项目的根目录的 ./node_modules文件夹里面,因此,为了提高webpack的查找速度,我们可以通过配置来指明第三方模块的存放路径,如下:

1
2
3
4
5
6
7
module.exports = {
resolve: {
// 使用绝对路径指明第三方模块存放的位置,以减少搜索步骤
// 其中 __dirname 表示当前工作目录,也就是项目根目录
modules: [path.resolve(__dirname, './node_modules')]
},
};

babel-loader的处理

由于ES6、ES7、ES8的新语法特性的快速普及,现代浏览器(chrome、safari、firefox、edge等)大多都已支持,因此我们可以使用这些新的特性进行编程,但是由于市面上各大浏览器厂商更新迭代过快,导致浏览器版本过多,对于新语法特性的支持也参差不齐,因此我们需要使用babel-loader进行语法转换成ES5,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
query: {
'presets': ['es2015', 'stage-0']
}
},
]
},
};

第三方模块构建处理

因为我们在文件里面可能会引入第三方模块来使用,第三方模块往往都是编译好的ES5的代码,并不需要loader再去处理,因此我们可以排除掉以提高loader的处理速度,如下:

1
2
// 这里也可使用正则,如:/node_modules/,webpack也可以识别
exclude: path.resolve(__dirname , './node_modules'),

缓存的处理

当我们的文件非常多的时候,构建的速度就会非常的慢了,babel-loader提供了一个cacheDirectory属性,这个属性默认是false不开启的,如果设置了这个参数并开启的话,被转换的结果将会被缓存起来,当webpack再次编译时,将会首先尝试从缓存中读取转换结果,以此避免资源浪费,使用如下:

1
use: ['babel-loader?cacheDirectory']

ES(6|7|8)新的API的处理

由于Babel默认只转换新的javascript句法(syntax),对于新的API是不做处理的,比如Iterator、Generator、Set、Maps、Symbol、Promise等这些全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码,这样如果浏览器不支持这些新的api,那么它们将不会工作,同时页面也将会报错误异常。

针对这个问题,我们可以使用bable-ployfill来处理,这里有个新的问题,babel-ployfill文件巨大,它默认把所有的新的API进行了重写,污染了全局变量,因此并不适合我们,针对现代浏览器,我们需要有针对的对不支持的API进行按需处理,因此便有了babel-plugin-transform-runtime,使用它loader在处理的时候,会自动的为所需要的API进行自动添加,不会去污染全局API,使用的时候我们需要先提前安装好,如下:

1
npm install babel-plugin-transform-runtime babel-preset-env babel-preset-stage-2 -S

在项目根目录新建.babelrc文件来进行配置,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"presets": [
["env", {
"modules": false
}],
"stage-2"
],
"plugins": [
["transform-runtime", {
"helpers": false,
"polyfill": false,
"moduleName": "babel-runtime"
}]
]
}

tree-shaking处理

当我们在项目里面使用ES6的module语法(export和import)的时候,需要告知babel-loader不要将ES6的模块先转成CommonJS模块,使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
test: /\.js$/,
use: {
loader: 'babel-loader',
query: {
'presets': [
['es2015', {'modules': false}], 'stage-0'
]
}
}

}

当然,我们也可以在项目根目录新建.babelrc文件来进行配置,如下:

1
2
3
4
5
6
7
8
{
"presets": [
["env", {
"modules": false
}],
"stage-0"
]
}

使用 ParallelUglifyPlugin

webpack默认提供了UglifyJS插件来压缩JS代码,但是它使用的是单线程压缩代码,也就是当我们有多个js需要压缩的时候,它需要一个个文件进行压缩。因此在构建的时候会非常慢(因为压缩JS代码需要先把代码解析成用Object抽象表示的AST语法树,再去应用各种规则分析和处理AST,导致这个过程耗时非常大)。

这个时候我们的ParallelUglifyPlugin就上场了,他会开启多个子进程,把对多个文件压缩的工作分别给多个子进程去完成,每个子进程其实还是通过 UglifyJS 去压缩代码,但是变成了并行执行,这样我们的构建效率就可以更快了,使用如下:

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
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')

module.exports = {
plugins: [
// 使用 ParallelUglifyPlugin 开启多进程并行文件压缩
new ParallelUglifyPlugin({
// 可以看到,内部还是使用uglifyjs进行压缩,参数通uglifyjs
uglifyJS: {
output: {
// 紧凑输出
beautify: false,
// 删除所有的注释
comments: false,
},
compress: {
// 在UglifyJs删除没有用到的代码时不输出警告
warnings: false,
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}
},
}),
],
}

在通过 new ParallelUglifyPlugin() 进行实例化时,支持以下参数:

  • test:使用正则去匹配哪些文件需要被 ParallelUglifyPlugin 压缩,默认是 /.js$/,也就是默认压缩所有的 .js 文件。
  • include:使用正则去命中需要被 ParallelUglifyPlugin 压缩的文件。默认为 []。
  • exclude:使用正则去命中不需要被 ParallelUglifyPlugin 压缩的文件。默认为 []。
  • cacheDir:缓存压缩后的结果,下次遇到一样的输入时直接从缓存中获取压缩后的结果并返回。cacheDir 用于配置缓存存放的目录路径。默认不会缓存,想开启缓存请设置一个目录路径。
  • workerCount:开启几个子进程去并发的执行压缩。默认是当前运行电脑的 CPU 核数减去1。
  • sourceMap:是否输出 Source Map,这会导致压缩过程变慢。
  • uglifyJS:用于压缩 ES5 代码时的配置,Object 类型,直接透传给 UglifyJS 的参数。
  • uglifyES:用于压缩 ES6 代码时的配置,Object 类型,直接透传给 UglifyES 的参数。

如下:

1
2
3
4
5
6
7
8
9
new ParallelUglifyPlugin({
uglifyJS: {},
test: /.js$/g,
include: [],
exclude: [],
cacheDir: '',
workerCount: '',
sourceMap: false
})

使用 HappyPack

happypack的工作原理是,告诉 HappyPack 核心调度器如何通过一系列 Loader 去转换一类文件,并且可以指定如何给这类转换操作分配子进程(和上面的ParallelUglifyPlugin类似)。

下面是一个相对比较全面的针对js、css、图片等的一个处理:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
const os = require('os');
const HappyPack = require('happypack');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')

module.exports = {
module: {
rules: [
{
test: /\.js$/,
// 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
use: ['happypack/loader?id=babel'],
// 排除 node_modules 目录下的文件,node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
exclude: path.resolve(__dirname, 'node_modules'),
},
{
// 把对 .css 文件的处理转交给 id 为 css 的 HappyPack 实例
test: /\.css$/,
use: ExtractTextPlugin.extract({
use: ['happypack/loader?id=css'],
}),
},
{
// 把对 图片类型 文件的处理转交给 id 为 image 的 HappyPack 实例
test: /\.(png|jpg|gif)$/,
use: 'happypack/loader?id=image'
}
]
},
plugins: [
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// 如何处理 .js 文件,用法和 Loader 配置中一样
loaders: ['babel-loader?cacheDirectory'],
// ... 其它配置项
}),
new HappyPack({
id: 'css',
// 如何处理 .css 文件,用法和 Loader 配置中一样
loaders: ['css-loader'],
}),
new HappyPack({
id: 'image',
// 如何处理 图片类型 文件,用法和 Loader 配置中一样
loaders: [{
loader : require.resolve('url-loader'),
options: {
limit : 10000,
name: 'static/images/[name].[ext]'
}
}],
threadPool: happyThreadPool
})
]
}

从上面可以看到,我们的配置有了一些改变,我们把文件的处理交给了happypack/loader来处理了,并且在后面还有一个id的参数,参数的值表示我们在使用happypack插件去实例化的时候该选择哪一个实例去处理

在插件(plugins)配置里面,我们实例化了三个happypack的实例,每个实例对应的都有一个id,它的值就是我们上面在处理loader的时候所携带的参数的值是一样相对应的,如?id=babel,loaders的属性配置也和原来使用的loader的配置相同

同样的,happypack还支持其它一些参数,如下:

  • threads 代表开启几个子进程去处理这一类型的文件,默认是3个,类型必须是整数(如果你的CPU够强悍,可以多开启几个子进程,构建速度可以快的飞起)。
  • verbose 是否允许 HappyPack 输出日志,默认是 true。
  • threadPool 代表共享进程池,即多个 HappyPack实例都使用同一个共享进程池中的子进程去处理任务,以防止资源占用过多

希望以上的一些构建性能优化的点,可以在大家使用webpack进行资源编译的时候能够有所帮助!