開發工具心得:如何 10 倍提升你的 Webpack 構建效率

0. 前言

babel+webpack+es6+react

圖1:ES6 + Webpack + React + Babelhtml

webpack 是個好東西,和 NPM 搭配起來使用管理模塊實在很是方便。而 Babel 更是神通常的存在,讓咱們在這個瀏覽器還沒有全面普及 ES6 語法的時代能夠先一步體驗到新的語法帶來的便利和效率上的提高。在 React 項目架構中這兩個東西基本成爲了標配,但 commonjs 的模塊必須在使用前通過 webpack 的構建(後文稱爲 build)才能在瀏覽器端使用,而每次修改也都須要從新構建(後文稱爲 rebuild)才能生效,如何提升 webpack 的構建效率成爲了提升開發效率的關鍵之一。node

1. Webpack 的構建流程

在開始正式的優化以前,讓咱們先回顧一下 Webpack 的構建流程,有哪些關鍵步驟,只有瞭解了這些,咱們才能分析出哪些地方有優化的可能性。
webpack officialreact

圖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

2. RESOLVE

咱們先從解析模塊路徑和分析依賴講起,有人可能以爲這無所謂,但當項目應用依賴的模塊愈來愈多,愈來愈重時,項目愈來愈大,文件和文件夾愈來愈多時,這個過程就變得愈來愈關乎性能。web

2.1 減少 Webpack 覆蓋的範圍

build +, rebuild +chrome

webpack 默認會去尋找全部 resolve.root 下的模塊,可是有些目錄咱們是能夠明確告知 webpack 不要管這裏,從而減輕 webpack 的工做量。這時會用到 module.noParse 參數。express

2.2 Resolove.root VS Resolove.moduledirectories

build +, rebuild +

rootmoduledirectories 若是隻從用法上來看,彷佛是能夠互相替代的。但由於 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 也很詳細地討論了這個問題。

3. LOADERS

webpack 官方和社區爲咱們提供了各類各樣 loader 來處理各類類型的文件,這些 loader 的配置也直接影響了構建的性能。

3.1 Babel-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']
              }
          }
        ]
    }
}

4. PLUGINS

webpack 官方和社區爲咱們提供了不少方便的插件,有些插件爲咱們開發和生產帶來了不少的便利,可是不合適地使用插件也會拖慢 webpack 的構建效率,而有些插件雖然不會爲咱們的開發上直接提供便利,但使用他們卻能夠幫助咱們提升 webpack 的構建效率,這也是本文會提到的。

4.1 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/do... ,咱們這裏推薦使用 cheap-source-map,也就是去掉了column mapping 和 loader-sourceMap(例如 jsx to js) 的 sourceMap,雖然帶上 eval 參數的能夠快更多,可是這種 sourceMap 只能看,不能調試,得不償失。

4.2 OPTIMIZATION

build ++,rebuild ++

webpack 提供了一些能夠優化瀏覽器端性能的優化插件,如UglifyJsPlugin,OccurrenceOrderPlugin 和 DedupePlugin,都很實用,也都在消耗構建性能(UglifyJsPlugin 很是耗性能),若是你是在開發環境下,這些插件最好都不要使用,畢竟腳本大一些,跑的慢一些這些比起每次構建要耗費更多時間來講,顯然仍是後者更會消磨開發者的耐心,所以,只在正產環境中使用 OPTIMIZATION。

4.3 CommonsChunk

rebuild +

當你的 webpack 構建任務中有多個入口文件,而這些文件都 require 了相同的模塊,若是你不作任何事情,webpack 會爲每一個入口文件引入一份相同的模塊,顯然這樣作,會使得相同模塊變化時,全部引入的 entry 都須要一次 rebuild,形成了性能的浪費,CommonsChunkPlugin 能夠將相同的模塊提取出來單獨打包,進而減少 rebuild 時的性能消耗。這裏有一篇很通俗易懂的使用方法:http://webpack.toobug.net/zh-... ,感興趣的朋友不妨一試。

4.4 DLL & DllReference

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 任務。

4.4.1 和 external 的比較

有人會說,這個和 用 webpackexternals 配置把 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>

這裏有兩個主要的區別:

  1. 像是 react 這種已經打好了生產包的使用 externals 很方便,可是也有不少 npm 包是沒有提供的,這種狀況下 DLLBundle 仍可使用。

  2. 若是隻是引入 npm 包一部分的功能,好比 require('react/lib/React') 或者 require('lodash/fp/extend') ,這種狀況下 DLLBundle 仍可使用。

  3. 固然若是隻是引用了 react 這類的話,externals 由於配置簡單因此也推薦使用。

4.5 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

  1. 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.

5. 其餘

上面咱們針對 webpack 的 resolve、loader 和 plugin 的過程給出了相應的優化意見,除了這些哪些優化點呢?其實有些優化貫穿在這個流程中,好比緩存和文件 IO。

5.1 Cache

不管在何種性能優化中,緩存老是必不可少的一部分,畢竟每次變更都隻影響很小的一部分,若是可以緩存住那些沒有變更的部分,直接拿來使用,天然會事半功倍,在 webpack 的整個構建過程當中,有多個地方提供了緩存的機會,若是咱們打開了這些緩存,會大大加速咱們的構建,尤爲是 rebuild 的效率。

5.1.1 webpack.cache

rebuild +

webpack 自身就有 cache 的配置,而且在 watch 模式下自動開啓,雖然效果不是最明顯的,但卻對全部的 module 都有效。

5.1.2 babel-loader.cacheDirectory

rebuild ++

babel-loader 能夠利用系統的臨時文件夾緩存通過 babel 處理好的模塊,對於 rebuild js 有着很是大的性能提高。

5.1.3 HappyPack.cache

build +, rebuild +

上面提到的 happyPack 插件也一樣提供了 cache 功能,默認是以 .happypack/cache--[id].json 的路徑進行緩存。由於是緩存在當前目錄下,因此他也能夠輔助下次 build 時的效率。

5.2 FileSystem

默認的狀況下,構建好的目錄必定要輸出到某個目錄下面才能使用,但 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);
   }
}

6. 總結

上面咱們從 webpack 構建的各個部分,給出了相應的優化策略,若是你的項目中可以將其徹底貫徹起來,10 倍提速不是夢想。這些優化也一樣應用到了咱們團隊的 react 項目中,https://github.com/uxcore/uxcore ,歡迎一塊兒來討論 webpack 的效率優化方案。

7. 參考文章

本文做者 eternalsky,始發於團隊微信公衆號 猿猿相抱 和我的博客 空の屋敷,轉載請保留做者信息。

相關文章
相關標籤/搜索