如何十倍提高你的webpack构建效率
前言
http://jafeney.com/2016/07/10/2016-07-10-webpack/ webpack
是个好东西,和 NPM
搭配起来使用管理模块实在非常方便。而 Babel
更是神一般的存在,让我们在这个浏览器尚未全面普及 ES6
语法的时代可以先一步体验到新的语法带来的便利和效率上的提升。在 React
项目架构中这两个东西基本成为了标配,但 commonjs
的模块必须在使用前经过 webpack
的构建(后文称为 build
)才能在浏览器端使用,而每次修改也都需要重新构建(后文称为 rebuild
)才能生效,如何提高 webpack
的构建效率成为了提高开发效率的关键之一。
Webpack 的构建流程
在开始正式的优化之前,让我们先回顾一下 Webpack
的构建流程,有哪些关键步骤,只有了解了这些,我们才能分析出哪些地方有优化的可能性。
首先,我们来看看官方对于 Webpack
的理念阐释,webapck
把所有的静态资源都看做是一个 module
,通过webpack
,将这些 module
组成到一个 bundle
中去,从而实现在页面上引入一个 bundle.js
,来实现所有静态资源的加载。所以详细一点看,webpack
应该是这样的:
通过 loader
,webpack
可以把各种非原生 js 的静态资源转换成 JavaScript
,所以理论上任何一种静态资源都可以成为一个 module
。
当然 webpack
还有很多其他好玩的特性,但不是本文的重点因此不铺开进行说明了。了解了上述的过程,我们就可以根据这些过程的前后处理进行对应的优化,接下来我们会针对 build 和 rebuild 的过程给与相应的意见。
RESOLVE
我们先从解析模块路径和分析依赖讲起,有人可能觉得这无所谓,但当项目应用依赖的模块越来越多,越来越重时,项目越来越大,文件和文件夹越来越多时,这个过程就变得越来越关乎性能。
减小 Webpack 覆盖的范围
build +, rebuild +
webpack
默认会去寻找所有 resolve.root
下的模块,但是有些目录我们是可以明确告知 webpack
不要管这里,从而减轻 webpack
的工作量。这时会用到 module.noParse
参数。
Resolove.root VS Resolove.moduledirectories
build +, rebuild +
root
和 moduledirectories
如果只从用法上来看,似乎是可以互相替代的。但因为 moduledirectories
从设计上是取相对路径,所以比起 root
,所以会多 parse
很多路径。
1
2
3
4
5
6
7
8
9
|
resolve: {
root: path.resolve('src'),
modulesDirectories: ['node_modules'],
extensions: ['', '.js', '.jsx']
},
resolve: {
modulesDirectories: ['node_modules', './src'],
extensions: ['', '.js', '.jsx']
},
|
上面的配置,只会解析 :
1
|
/some/src/other/folder/node_modules/a
|
而下面的配置会解析 :
1
2
3
4
5
6
7
8
|
/some/folder/structure/node_modules/a
/some/folder/structure/src/a
/some/folder/node_modules/a
/some/folder/src/a
/some/node_modules/a
/some/src/a
/node_modules/a
/src/a
|
大部分的情况下使用 root
即可,只有在有很复杂的路径下,才考虑使用 moduledirectories
,这可以明显提高webpack
的构建性能。这个 issue
也很详细地讨论了这个问题。
LOADERS
webpack
官方和社区为我们提供了各种各样 loader
来处理各种类型的文件,这些 loader
的配置也直接影响了构建的性能。
Babel-loader: 能者少劳
build ++, rebuild ++
以 babel-loader
为例,我们在开发 React
项目时很可能会使用到了 ES6 或者 jsx 的语法,因此使用到 babel-loader
的情况很多,最简单的情况下我们可以这样配置,让所有的 js/jsx
通过 babel-loader
:
1
2
3
4
5
6
7
8
9
10
11
|
module: {
loaders: [
{
test: /\.js(x)*$/,
loader: 'babel-loader',
query: {
presets: ['react', 'es2015-ie', 'stage-1']
}
}
]
}
|
上面这样的做法当然是 ok
的,但是对于很多的 npm
包来说,他们完全没有经过 babel
的必要(成熟的 npm
包会在发布前将自己 es5
,甚至 es3
化),让这些包通过 babel
会带来巨大的性能负担,毕竟 babel6
要经过几十个插件的处理,虽然 babel-loader
强大,但能者多劳的这种保守的想法却使得 babel-loader
成为了整个构建的性能瓶颈。所以我们可以使用 exclude
,大胆地屏蔽掉 npm
里的包,从而使整包的构建效率飞速提高。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
module: {
loaders: [
{
test: /\.js(x)*$/,
loader: 'babel-loader',
exclude: function(path) {
// 路径中含有 node_modules 的就不去解析。
var isNpmModule = !!path.match(node_modules/);
return isNpmModule;
},
query: {
presets: ['react', 'es2015-ie', 'stage-1']
}
}
]
}
|
甚至,在我们十分确信的情况下,使用 include
来限定 babel
的使用范围,进一步提高效率。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
var path = require('path');
module.exports = {
module: {
loaders: [
{
test: /\.js(x)*$/,
loader: 'babel-loader',
include: [
// 只去解析运行目录下的 src 和 demo 文件夹
path.join(process.cwd(), './src'),
path.join(process.cwd(), './demo')
],
query: {
presets: ['react', 'es2015-ie', 'stage-1']
}
}
]
}
}
|
PLUGINS
webpack
官方和社区为我们提供了很多方便的插件,有些插件为我们开发和生产带来了很多的便利,但是不合适地使用插件也会拖慢 webpack
的构建效率,而有些插件虽然不会为我们的开发上直接提供便利,但使用他们却可以帮助我们提高 webpack
的构建效率,这也是本文会提到的。
SourceMaps
build +
SourceMaps
是一个非常实用的功能,可以让我们在 chrome debug
时可以不用直接看已经 bundle
过的 js,而是直接在源代码上进行查看和调试,但完美的 SourceMaps
是很慢的,webpack
官方提供了七种 sourceMap
模式共大家选择,性能对比如下:
devtool | build speed | rebuild speed | production supported | quality |
---|---|---|---|---|
eval | +++ | +++ | no | generated code |
cheap-eval-source-map | + | ++ | no | transformed code (lines only) |
cheap-source-map | + | o | yes | transformed code (lines only) |
cheap-module-eval-source-map | o | ++ | no | original source (lines only) |
cheap-module-source-map | o | - | yes | original source (lines only) |
eval-source-map | – | + | no | original source |
source-map | – | – | yes | original source |
具体各自的区别请参考 https://github.com/webpack/docs/wiki/configuration#devtool ,我们这里推荐使用 cheap-source-map,也就是去掉了column mapping 和 loader-sourceMap(例如 jsx to js) 的 sourceMap,虽然带上 eval 参数的可以快更多,但是这种 sourceMap 只能看,不能调试,得不偿失。
OPTIMIZATION
build ++,rebuild ++
webpack 提供了一些可以优化浏览器端性能的优化插件,如UglifyJsPlugin,OccurrenceOrderPlugin 和 DedupePlugin,都很实用,也都在消耗构建性能(UglifyJsPlugin 非常耗性能),如果你是在开发环境下,这些插件最好都不要使用,毕竟脚本大一些,跑的慢一些这些比起每次构建要耗费更多时间来说,显然还是后者更会消磨开发者的耐心,因此,只在正产环境中使用 OPTIMIZATION。
CommonsChunk
rebuild +
当你的 webpack
构建任务中有多个入口文件,而这些文件都 require
了相同的模块,如果你不做任何事情,webpack
会为每个入口文件引入一份相同的模块,显然这样做,会使得相同模块变化时,所有引入的 entry
都需要一次 rebuild
,造成了性能的浪费,CommonsChunkPlugin
可以将相同的模块提取出来单独打包,进而减小 rebuild 时的性能消耗。这里有一篇很通俗易懂的使用方法:http://webpack.toobug.net/zh-cn/chapter3/common-chunks-plugin.html ,感兴趣的朋友不妨一试。
DLL & DllReference
build +++, rebuild +++
除了正在开发的源代码之外,通常还会引入很多第三方 NPM
包,这些包我们不会进行修改,但是仍然需要在每次build
的过程中消耗构建性能,那有没有什么办法可以减少这些消耗呢?DLLPlugin
就是一个解决方案,他通过前置这些依赖包的构建,来提高真正的 build
和 rebuild
的构建效率。 鉴于现有的资料对于这两个插件的解释都不是很清楚,笔者这里翻译了一篇日本同学的文章,通过一个简单的例子来说明一下这两个插件的用法。我们举例,把react
和 react-dom
打包成为 dll bundle
。 首先,我们来写一个 DLLPlugin
的 config
文件。
webpack.dll.config.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
27
28
29
30
31
32
33
34
|
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: {
vendor: ['react', 'react-dom']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].dll.js',
/**
* output.library
* 将会定义为 window.${output.library}
* 在这次的例子中,将会定义为`window.vendor_library`
*/
library: '[name]_library'
},
plugins: [
new webpack.DllPlugin({
/**
* path
* 定义 manifest 文件生成的位置
* [name]的部分由entry的名字替换
*/
path: path.join(__dirname, 'dist', '[name]-manifest.json'),
/**
* name
* dll bundle 输出到那个全局变量上
* 和 output.library 一样即可。
*/
name: '[name]_library'
})
]
};
|
执行 webpack
后,就会在 dist
目录下生成 dll bundle
和对应的 manifest
文件
1
2
3
4
5
6
7
8
9
10
11
12
|
$ ./node_modules/.bin/webpack --config webpack.dll.config.js
Hash: 36187493b1d9a06b228d
Version: webpack 1.13.1
Time: 860ms
Asset Size Chunks Chunk Names
vendor.dll.js 699 kB 0 [emitted] vendor
[0] dll vendor 12 bytes {0} [built]
+ 167 hidden modules
$ ls dist
./ vendor-manifest.json
../ vendor.dll.js
|
manifest
文件的格式大致如下,由包含的 module
和对应的 id
的键值对构成。
1
2
3
4
5
6
7
8
9
10
11
12
|
cat dist/vendor-manifest.json
{
"name": "vendor_library",
"content": {
"./node_modules/react/react.js": 1,
"./node_modules/react/lib/React.js": 2,
"./node_modules/process/browser.js": 3,
"./node_modules/object-assign/index.js": 4,
"./node_modules/react/lib/ReactChildren.js": 5,
"./node_modules/react/lib/PooledClass.js": 6,
"./node_modules/fbjs/lib/invariant.js": 7,
...
|
好,接下来我们通过 DLLReferencePlugin
来使用刚才生成的 DLL Bundle
。
首先我们写一个只去 require react
,并通过 console.log
吐出的 index.js
。
1
2
3
4
|
var React = require('react');
var ReactDOM = require('react-dom');
console.log("dll's React:", React);
console.log("dll's ReactDOM:", ReactDOM);
|
再写一个不参考 Dll Bundle
的普通 webpack config
文件。
webpack.conf.js
1
2
3
4
5
6
7
8
9
10
11
12
|
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: {
'dll-user': ['./index.js']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].bundle.js'
}
};
|
执行 webpack
,会在 dist
下生成 dll-user.bundle.js
,约 700K,耗时 801ms。
1
2
3
4
5
6
7
8
9
|
$ ./node_modules/.bin/webpack
Hash: d8cab39e58c13b9713a6
Version: webpack 1.13.1
Time: 801ms
Asset Size Chunks Chunk Names
dll-user.bundle.js 700 kB 0 [emitted] dll-user
[0] multi dll-user 28 bytes {0} [built]
[1] ./index.js 145 bytes {0} [built]
+ 167 hidden modules
|
接下来,我们加入 DLLReferencePlugin
webpack.conf.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 = {
entry: {
'dll-user': ['./index.js']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].bundle.js'
},
// ----在这里追加----
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
/**
* 在这里引入 manifest 文件
*/
manifest: require('./dist/vendor-manifest.json')
})
]
// ----在这里追加----
};
|
1
2
3
4
5
6
7
8
9
|
./node_modules/.bin/webpack
Hash: 3bc7bf760779b4ca8523
Version: webpack 1.13.1
Time: 70ms
Asset Size Chunks Chunk Names
dll-user.bundle.js 2.01 kB 0 [emitted] dll-user
[0] multi dll-user 28 bytes {0} [built]
[1] ./index.js 145 bytes {0} [built]
+ 3 hidden modules
|
结果是非常惊人的,只有2.01K,耗时 70 ms,无疑大大提高了 build
和 rebuild
的效率。实际放到页面上看下是否可行。
1
2
3
4
|
<body>
<script src="dist/vendor.dll.js"></script>
<script src="dist/dll-user.bundle.js"></script>
</body>
|
因为 Dll bundle
在依赖安装完毕后就可以进行了,我们可以在第一次执行 dev server
前执行一次 dll bundle
的webapck
任务。
和 external 的比较
有人会说,这个和 用 webpack
的 externals
配置把 require
的 module
指向全局变量有点像啊。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: {
'ex': ['./index.js']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].bundle.js'
},
externals: {
// require('react')はwindow.Reactを使う
'react': 'React',
// require('react-dom')はwindow.ReactDOMを使う
'react-dom': 'ReactDOM'
}
};
|
1
2
3
4
5
|
<body>
<script src="dist/react.min.js"></script>
<script src="dist/react-dom.min.js"></script>
<script src="dist/ex.bundle.js"></script>
</body>
|
这里有两个主要的区别:
- 像是
react
这种已经打好了生产包的使用externals
很方便,但是也有很多npm
包是没有提供的,这种情况下DLLBundle
仍可以使用。- 如果只是引入
npm
包一部分的功能,比如require('react/lib/React')
或者require('lodash/fp/extend')
,这种情况下DLLBundle
仍可以使用。- 当然如果只是引用了
react
这类的话,externals
因为配置简单所以也推荐使用。
HappyPack
build +, rebuild +
webpack
的长时间构建搞的大家都很 unhappy
。于是 @amireh
想到了一个点子,既然 loader
默认都是一个进程在跑,那是否可以让 loader
多进程去处理文件呢?
happyPack
的文档写的很易懂,这里就不再赘述,happyPack
不仅利用了多进程,同时还利用缓存来使得 rebuild
更快。下面是插件作者给出的性能数据:
For the main repository I tested on, which had around 3067 modules, the build time went down from 39 seconds to a whopping ~10 seconds when there was yet no cache. Successive builds now take between 6 and 7 seconds.
Here’s a rundown of the various states the build was performed in:
Elapsed (ms) | Happy? | Cache enabled? | Cache present? | Using DLLs? |
---|---|---|---|---|
39851 | NO | N/A | N/A | NO |
37393 | NO | N/A | N/A | YES |
14605 | YES | NO | N/A | NO |
13925 | YES | YES | NO | NO |
11877 | YES | YES | YES | NO |
9228 | YES | NO | N/A | YES |
9597 | YES | YES | NO | YES |
6975 | YES | YES | YES | YES |
The builds above were run on Linux over a machine with 12 cores.
其他
上面我们针对 webpack
的 resolve
、loader
和 plugin
的过程给出了相应的优化意见,除了这些哪些优化点呢?其实有些优化贯穿在这个流程中,比如缓存和文件 IO。
Cache
无论在何种性能优化中,缓存总是必不可少的一部分,毕竟每次变动都只影响很小的一部分,如果能够缓存住那些没有变动的部分,直接拿来使用,自然会事半功倍,在 webpack 的整个构建过程中,有多个地方提供了缓存的机会,如果我们打开了这些缓存,会大大加速我们的构建,尤其是 rebuild 的效率。
webpack.cache
rebuild +
webpack
自身就有 cache
的配置,并且在 watch
模式下自动开启,虽然效果不是最明显的,但却对所有的module
都有效。
babel-loader.cacheDirectory
rebuild ++
babel-loader
可以利用系统的临时文件夹缓存经过 babel 处理好的模块,对于 rebuild js
有着非常大的性能提升。
HappyPack.cache
build +, rebuild +
上面提到的 happyPack
插件也同样提供了 cache
功能,默认是以 .happypack/cache--[id].json
的路径进行缓存。因为是缓存在当前目录下,所以他也可以辅助下次 build
时的效率。
FileSystem
默认的情况下,构建好的目录一定要输出到某个目录下面才能使用,但 webpack
提供了一种很棒的读写机制,使得我们可以直接在内存中进行读写,从而极大地提高 IO 的效率,开启的方法也很简单。
1
2
3
4
5
6
7
8
9
10
|
var MemoryFS = require("memory-fs");
var webpack = require("webpack");
var fs = new MemoryFS();
var compiler = webpack({ ... });
compiler.outputFileSystem = fs;
compiler.run(function(err, stats) {
// ...
var fileContent = fs.readFileSync("...");
});
|
当然,我们还可以通过 webpackDevMiddleware
更加无缝地就接入到 dev server
中,例如我们以 express
作为静态 server
的例子。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
var compiler = webpack(webpackCfg);
var webpackDevMiddlewareInstance = webpackDevMiddleware(compiler, {
// webpackDevMiddleware 默认使用了 memory-fs
publicPath: '/dist',
aggregateTimeout: 300, // wait so long for more changes
poll: true, // use polling instead of native watchers
stats: {
chunks: false
}
});
var app = express();
app.use(webpackDevMiddlewareInstance);
app.listen(xxxx, function(err) {
console.log(colors.info("dev server start: listening at " + xxxx));
if (err) {
console.error(err);
}
}
|
总结
上面我们从 webpack
构建的各个部分,给出了相应的优化策略,如果你的项目中能够将其完全贯彻起来,10 倍提速不是梦想。