图1:ES6 + Webpack + React + Babelhtml
webpack 是个好东西,和 NPM 搭配起来使用管理模块实在很是方便。而 Babel 更是神通常的存在,让咱们在这个浏览器还没有全面普及 ES6 语法的时代能够先一步体验到新的语法带来的便利和效率上的提高。在 React 项目架构中这两个东西基本成为了标配,但 commonjs 的模块必须在使用前通过 webpack 的构建(后文称为 build)才能在浏览器端使用,而每次修改也都须要从新构建(后文称为 rebuild)才能生效,如何提升 webpack 的构建效率成为了提升开发效率的关键之一。node
在开始正式的优化以前,让咱们先回顾一下 Webpack 的构建流程,有哪些关键步骤,只有了解了这些,咱们才能分析出哪些地方有优化的可能性。react
图2:webpack is a module bundler.webpack
首先,咱们来看看官方对于 Webpack 的理念阐释,webapck 把全部的静态资源都看作是一个 module,经过 webpack,将这些 module 组成到一个 bundle 中去,从而实如今页面上引入一个 bundle.js,来实现全部静态资源的加载。因此详细一点看,webpack 应该是这样的:git
图3:Every static asset should be able to be a module --webpackes6
经过 loader,webpack 能够把各类非原生 js 的静态资源转换成 JavaScript,因此理论上任何一种静态资源均可以成为一个 module。
固然 webpack 还有不少其余好玩的特性,但不是本文的重点所以不铺开进行说明了。了解了上述的过程,咱们就能够根据这些过程的先后处理进行对应的优化,接下来咱们会针对 build 和 rebuild 的过程给与相应的意见。github
咱们先从解析模块路径和分析依赖讲起,有人可能以为这无所谓,但当项目应用依赖的模块愈来愈多,愈来愈重时,项目愈来愈大,文件和文件夹愈来愈多时,这个过程就变得愈来愈关乎性能。web
build +, rebuild +chrome
webpack 默认会去寻找全部 resolve.root 下的模块,可是有些目录咱们是能够明确告知 webpack 不要管这里,从而减轻 webpack 的工做量。这时会用到 module.noParse
参数。express
build +, rebuild +
root
和 moduledirectories
若是只从用法上来看,彷佛是能够互相替代的。但由于 moduledirectories
从设计上是取相对路径,因此比起 root
,因此会多 parse 不少路径。
resolve: { root: path.resolve('src/node_modules'), extensions: ['', '.js', '.jsx'] }, resolve: { modulesDirectories: ['node_modules', './src'], extensions: ['', '.js', '.jsx'] },
上面的配置,只会解析
./src/node_modules/a
==== 此处有修改 2016/09/10 感谢 @lili_21 ====
而下面的配置会解析
/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 也很详细地讨论了这个问题。
webpack 官方和社区为咱们提供了各类各样 loader 来处理各类类型的文件,这些 loader 的配置也直接影响了构建的性能。
build ++, rebuild ++
以 babel-loader 为例,咱们在开发 React 项目时极可能会使用到了 ES6 或者 jsx 的语法,所以使用到 babel-loader 的状况不少,最简单的状况下咱们能够这样配置,让全部的 js/jsx 经过 babel-loader:
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 里的包,从而使整包的构建效率飞速提升。
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 的使用范围,进一步提升效率。
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'] } } ] } }
webpack 官方和社区为咱们提供了不少方便的插件,有些插件为咱们开发和生产带来了不少的便利,可是不合适地使用插件也会拖慢 webpack 的构建效率,而有些插件虽然不会为咱们的开发上直接提供便利,但使用他们却能够帮助咱们提升 webpack 的构建效率,这也是本文会提到的。
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/do... ,咱们这里推荐使用 cheap-source-map,也就是去掉了column mapping 和 loader-sourceMap(例如 jsx to js) 的 sourceMap,虽然带上 eval
参数的能够快更多,可是这种 sourceMap 只能看,不能调试,得不偿失。
build ++,rebuild ++
webpack 提供了一些能够优化浏览器端性能的优化插件,如UglifyJsPlugin,OccurrenceOrderPlugin 和 DedupePlugin,都很实用,也都在消耗构建性能(UglifyJsPlugin 很是耗性能),若是你是在开发环境下,这些插件最好都不要使用,毕竟脚本大一些,跑的慢一些这些比起每次构建要耗费更多时间来讲,显然仍是后者更会消磨开发者的耐心,所以,只在正产环境中使用 OPTIMIZATION。
rebuild +
当你的 webpack 构建任务中有多个入口文件,而这些文件都 require 了相同的模块,若是你不作任何事情,webpack 会为每一个入口文件引入一份相同的模块,显然这样作,会使得相同模块变化时,全部引入的 entry 都须要一次 rebuild,形成了性能的浪费,CommonsChunkPlugin 能够将相同的模块提取出来单独打包,进而减少 rebuild 时的性能消耗。这里有一篇很通俗易懂的使用方法:http://webpack.toobug.net/zh-... ,感兴趣的朋友不妨一试。
build +++, rebuild +++
除了正在开发的源代码以外,一般还会引入不少第三方 NPM 包,这些包咱们不会进行修改,可是仍然须要在每次 build 的过程当中消耗构建性能,那有没有什么办法能够减小这些消耗呢?DLLPlugin 就是一个解决方案,他经过前置这些依赖包的构建,来提升真正的 build 和 rebuild 的构建效率。
鉴于现有的资料对于这两个插件的解释都不是很清楚,笔者这里翻译了一篇日本同窗的文章,经过一个简单的例子来讲明一下这两个插件的用法。咱们举例,把 react 和 react-dom 打包成为 dll bundle。
首先,咱们来写一个 DLLPlugin 的 config 文件。
webpack.dll.config.js
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 文件
$ ./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 的键值对构成。
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
。
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
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。
$ ./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
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') }) ] // ----在这里追加---- };
./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 的效率。实际放到页面上看下是否可行。
<body> <script src="dist/vendor.dll.js"></script> <script src="dist/dll-user.bundle.js"></script> </body>
由于 Dll bundle 在依赖安装完毕后就能够进行了,咱们能够在第一次执行 dev server 前执行一次 dll bundle 的 webapck 任务。
有人会说,这个和 用 webpack
的 externals
配置把 require 的 module 指向全局变量有点像啊。
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' } };
<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
由于配置简单因此也推荐使用。
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
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。
不管在何种性能优化中,缓存老是必不可少的一部分,毕竟每次变更都只影响很小的一部分,若是可以缓存住那些没有变更的部分,直接拿来使用,天然会事半功倍,在 webpack 的整个构建过程当中,有多个地方提供了缓存的机会,若是咱们打开了这些缓存,会大大加速咱们的构建,尤为是 rebuild 的效率。
rebuild +
webpack 自身就有 cache 的配置,而且在 watch 模式下自动开启,虽然效果不是最明显的,但却对全部的 module 都有效。
rebuild ++
babel-loader 能够利用系统的临时文件夹缓存通过 babel 处理好的模块,对于 rebuild js 有着很是大的性能提高。
build +, rebuild +
上面提到的 happyPack 插件也一样提供了 cache 功能,默认是以 .happypack/cache--[id].json
的路径进行缓存。由于是缓存在当前目录下,因此他也能够辅助下次 build 时的效率。
默认的状况下,构建好的目录必定要输出到某个目录下面才能使用,但 webpack 提供了一种很棒的读写机制,使得咱们能够直接在内存中进行读写,从而极大地提升 IO 的效率,开启的方法也很简单。
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 的例子。
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 倍提速不是梦想。这些优化也一样应用到了咱们团队的 react 项目中,https://github.com/uxcore/uxcore ,欢迎一块儿来讨论 webpack 的效率优化方案。
webpack build performance:http://webpack.github.io/docs...
webpackのDLLバンドルを使ってビルドを速くする:http://qiita.com/pirosikick/i...
How to make your Webpack builds 10x faster:http://www.slideshare.net/tru...
本文做者 eternalsky,始发于团队微信公众号 猿猿相抱 和我的博客 空の屋敷,转载请保留做者信息。