JavaScript
webpack

webpackのDLLバンドルを使ってビルドを速くする

More than 1 year has passed since last update.

How to make your Webpack builds 10x faster

「webpackビルドを10倍速くする方法」というスライドを見つけた。

内容を要約すると、こんな感じ。

  • css-loaderは0.15未満を使う
  • cacheDiretoryはデフォルトで無効だから有効にする
  • HappyPackを使う
  • DLLバンドルを使って、静的コードのバンドルを分ける

DLLバンドルは聞いたことがなかったので調べた。

DLLバンドルとは

Dll bundles doesn't execute any of your module's code. They only include modules.

モジュールをまとめただけのbundleで、scriptタグで読み込んだ時点では含まれるモジュールは実行されず、他のbundleから参照された時に実行される。

<!-- こういうイメージ -->
<script src="vendor.dll.js" />
<script src="a.bundle.js" />

DLLバンドルの作り方

DllPlugin

webpack.DllPluginを使って、DLLバンドルと、そのDLLバンドルに含まれるモジュールの情報が書かれたmanifestファイル(jsonファイル)を生成する。

サンプル

例として、reactreact-domを含んだdll bundleを作る。

まず、必要なパッケージをインストールする。webpackは全部入りなので、楽。

$ mkdir webpack-dll-example && cd webpack-dll-example
$ echo "{}" > package.json
$ npm install -D webpack
$ npm install -S react react-dom

webpack.config.jsを書く。後でもう一つwebpack.configを書くので、webpack.dll.config.jsという名前にする。

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
       * どの空間(global変数)にdll bundleがあるか
       * output.libraryに指定した値を使えばよい
       */
      name: '[name]_library'
    })
  ]
};

webpackを実行すると、dist以下にDLLバンドルと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ファイルは↓のような感じで、含まれるモジュールとそのidがkey-valueで定義されている。

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

DLLバンドルの使い方

DllReferencePlugin

作ったDLLバンドルを別のバンドルから参照するには、webpack.DllReferencePluginを使う。
ビルド時にDllReferencePluginを通してDLLバンドルのmanifestファイルを読み込むと、requireの解決の際にDLLバンドルに含まれるモジュールを使ってくれるようになる。

サンプル

さっきと同じディレクトリで作業する。

まず、reactをrequireして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バンドルを参照しない、普通のwebpack.config.jsを書く。

webpack.config.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が出力される。700KB。

terminal
$ ./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

では、webpack.config.jsにDllReferencePluginを追加してビルドする。

webpack.config.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')
    })
  ]
  // ----ここまで追加---
};
terminal
./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.01KB。めっちゃ軽くなった。

実際に下記のようなhtmlを書いて、react, react-domがロードできているか(ブラウザのコンソールに出力されているか)を確認。

<body>
  <script src="dist/vendor.dll.js"></script>
  <script src="dist/dll-user.bundle.js"></script>
</body>

スクリーンショット 2016-05-27 11.31.06.png

ちゃんと出力されている。うえーい。

実際どれくらい速くなるか?

react, fluxibleで作っているツールで試してみた。DLLバンドルにはreact, react-dom, fluxibleなど、npmでインストールしたモジュールを全部突っ込む。

terminal
// 導入前
$ webpack --config webpack.config.dev.js
Hash: e30bcddcf9b7d11ac9f8
Version: webpack 1.12.14
Time: 6684ms
           Asset     Size  Chunks             Chunk Names
client.bundle.js  4.02 MB       0  [emitted]  client
 login.bundle.js  2.44 MB       1  [emitted]  login
   [0] multi login 28 bytes {1} [built]
   [0] multi client 52 bytes {0} [built]
    + 624 hidden modules

// 導入後
$ webpack --config webpack.config.dev.js
Hash: 26b6e5664a6eff083097
Version: webpack 1.12.14
Time: 4478ms
           Asset     Size  Chunks             Chunk Names
client.bundle.js  1.31 MB       0  [emitted]  client
 login.bundle.js   707 kB       1  [emitted]  login
   [0] multi login 28 bytes {1} [built]
   [0] multi client 52 bytes {0} [built]
    + 277 hidden modules

2.2秒ほど速くなった。

おまけ:cacheDirectory

ついでなので、元スライドで紹介されていたcacheDiretoryも有効にしてみる。(cacheDirectoryとbabel-loaderのqueryにcacheDirectoryを追加)

webpack --config webpack.config.dev.js
Hash: 26b6e5664a6eff083097
Version: webpack 1.12.14
Time: 2248ms
           Asset     Size  Chunks             Chunk Names
client.bundle.js  1.31 MB       0  [emitted]  client
 login.bundle.js   707 kB       1  [emitted]  login
   [0] multi login 28 bytes {1} [built]
   [0] multi client 52 bytes {0} [built]
    + 277 hidden modules

更に2秒速くなった。babelのコンパイルに時間がかかっている気がするので、HappyPackを入れると更に速くなりそう。

externalsと比較

externalsを使うとglobal変数からにモジュールをrequireでき、DLLと似たようなことができる。

例えば、Reactの場合、下記をwebpack.configに書くと、require('react')window.Reactから取得するようになる。

webpack.config.js
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'
  }
};

下記のようにreact.min.jsとreact-dom.min.jsをバンドルより先に読みこめばよい。

<body>
  <script src="dist/react.min.js"></script>
  <script src="dist/react-dom.min.js"></script>
  <script src="dist/ex.bundle.js"></script>
</body>

dllとの違いは、

  • reactのように既にビルドされているJS(react.min.js)がある場合はexternalsでも十分だが、fluxibleのようにビルドされたJSがない場合はdllにしないとバンドルを分けることができない。
  • dllはモジュールのファイルパスも保持しているので、dllに含まれていればrequire('react/lib/React')のように、一部のモジュールだけrequireできる。

たぶんこんな感じ。externalsで済むときはそちらの方が設定が楽なのでおすすめ。

dllはcommonjsでロードできればOKなので、2つのSPAがありentryが分かれているような場合に、共通するモジュールだけdllに寄せる等の用途に使えそう。