前言

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 应该是这样的:

通过 loaderwebpack 可以把各种非原生 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>

这里有两个主要的区别:

  1. 像是 react 这种已经打好了生产包的使用 externals 很方便,但是也有很多 npm 包是没有提供的,这种情况下DLLBundle 仍可以使用。
  2. 如果只是引入 npm 包一部分的功能,比如 require('react/lib/React') 或者 require('lodash/fp/extend'),这种情况下 DLLBundle 仍可以使用。
  3. 当然如果只是引用了 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 的 resolveloader 和 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 倍提速不是梦想。

如何十倍提高你的webpack构建效率的更多相关文章

  1. 通过非聚集索引让select count(*) from 的查询速度提高几十倍、甚至千倍

    通过非聚集索引,可以显著提升count(*)查询的性能. 有的人可能会说,这个count(*)能用上索引吗,这个count(*)应该是通过表扫描来一个一个的统计,索引有用吗? 不错,一般的查询,如果用 ...

  2. 我是如何让minio client上传速度提高几十倍的

    minio java client 使用okhttp作为底层的http实现,在产品包里面局域网上传文件的速度一直只有400~800KB/s,经过一天排查发现是-Djava.compile=none禁用 ...

  3. fasthttp:比net/http快十倍的Go框架(server 篇)

    转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com/archives/574 我们在上一篇文章中讲解了 Go HTTP 标准库的实现原理,这 ...

  4. webpack构建具备版本管理能力的项目

    webpack是时下十分流行的编译和打包工具,它提供一种可扩展的loader的方式,简单的配置,便可以编译打包各类型的文件,包括js.css.image.font.html,以及各种预编译语言都不在话 ...

  5. 优化Webpack构建性能的几点建议

    Webpack 作为目前最流行的前端构建工具之一,在 vue/react 等 Framework 的生态圈中都占据重要地位.在开发现代 Web 应用的过程中,Webpack 和我们的开发过程和发布过程 ...

  6. 十倍效能提升——Web 基础研发体系的建立

    1 导读 web 基础研发体系指的是, web 研发中一线工程师所直接操作的技术.工具,以及所属组织架构的总和.在过去提升企业研发效能的讨论中,围绕的主题基本都是——”通过云计算.云存储等方式将底层核 ...

  7. 深入浅出的webpack构建工具---AutoDllPlugin插件(八)

    深入浅出的webpack构建工具---AutoDllPlugin插件(八) DllPlugin插件能够快速打包,能把第三方依赖的文件能提前进行预编译打包到一个文件里面去.提高了构建速度.因为很多第三方 ...

  8. 深入浅出的webpack构建工具---webpack基本配置(一)

    深入浅出的webpack构建工具---webpack基本配置(一) 阅读目录 一:webpack入门构建: 1. 安装webpack到全局 2. 安装webpack到本项目. 3. 如何使用webpa ...

  9. [No0000D0] 让你效率“猛增十倍”,沉浸工作法到底是什么?

    一位编剧在三天内完成两万字的剧本,而在此之前,他曾拖延了足足半年.一名大四学生用一天半写了8000多字,一鼓作气拿下毕业论文. 有人说:“用了这个方法,我的效率猛增十倍.只用短短两小时,就摧枯拉朽地完 ...

随机推荐

  1. Formal Analysis of the TLS Handshake Protocol -----论文整理

    1.关键词  TLS.SSL.Formal Analsysis  Conridentiality  Secerecy 2.Table  THE SSL/TLS handshake Protocol 3 ...

  2. STM32复位及通过函数判断是何种条件出发的复位

    STM32F10xxx支持三种复位形式,分别为系统复位.上电复位和备份区域复位. 一.系统复位: 系统复位将复位所有寄存器至它们的复位状态. 当发生以下任一事件时,产生一个系统复位: 1. NRST引 ...

  3. linux----centos7 yum安装lnmp+zabbix

    安装yum utils工具包,若不安装则会找不到命令yum-config-manageryum -y install yum-utils 启用yum仓库yum-config-manager --ena ...

  4. datatable修改每页默认显示的数量

    datatable修改每页默认显示的数量 一.总结 一句话总结: iDisplayLength属性:'iDisplayLength':50 1.datatable默认每页显示50个? iDisplay ...

  5. SQL进阶17-变量的声明/使用(输出)--全局变量/会话变量--用户变量/局部变量

    /*进阶17 变量 系统变量: 全局变量: 会话变量: 自定义变量: 用户变量: 局部变量: */ /* #一: 系统变量 #说明: 变量由系统提供,不是用户定义的,属于服务器层面 #使用的语法 #1 ...

  6. (一)python3.7的安装

    1.从官网https://www.python.org/下载相应版本的安装包.一般下载 executable installer,x86 表示是 32 位的,x86-64 表示 64 位的. 2.可选 ...

  7. 文件读写(一)利用File静态类 System.IO.FileInfo、DirectoryInfo、DriveInfo

    提供用于创建.复制.删除.移动和打开单一文件的静态方法,并协助创建 FileStream 对象. 一.读文件: 1.返回字符串:File.ReadAllText() string readText = ...

  8. Git报错:Permission denied (publickey)

    Git在克隆的时候报错.Permission denied (publickey). 报错 Permission denied (publickey) 具体如下: 原因:没有将自己的电脑的SSH ke ...

  9. 物联网之窄带物联网(NB-IOT)

    NB-IoT即窄带物联网(Narrow Band Internet of Things),NB-IOT构建在蜂窝网络之上,只消耗大约180KHZ的带宽,可直接部署于GSM(2G).UMTS(3G).L ...

  10. [Dart] Dynamic variable in Dart

    First way to create dynamic variable is using 'dymaic' keywrod: dynamic a = 123; a = '123'; Second w ...