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


[How to make your Webpack builds 10x faster](http://www.slideshare.net/trueter/how-to-make-your-webpack-builds-10x-faster)

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

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

- css-loaderは0.15未満を使う
- cacheDiretoryはデフォルトで無効だから有効にする
- [HappyPack](https://github.com/amireh/happypack)を使う
- DLLバンドルを使って、静的コードのバンドルを分ける

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

## DLLバンドルとは

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

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

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

## DLLバンドルの作り方

[DllPlugin](https://github.com/webpack/docs/wiki/list-of-plugins#dllplugin)

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

### サンプル

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

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

```sh
$ 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`という名前にする。

```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ファイルを出力される。

```terminal
$ ./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で定義されている。

```sh
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](https://github.com/webpack/docs/wiki/list-of-plugins#dllreferenceplugin)

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

### サンプル

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

まず、reactをrequireしてconsole.logに吐くだけの`index.js`を書く。

```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を書く。

```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。

```sh: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を追加してビルドする。

```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'
  },
  // ----ここから追加---
  plugins: [
    new webpack.DllReferencePlugin({
      context: __dirname,
      /**
       * manifestファイルをロードして渡す
       */
      manifest: require('./dist/vendor-manifest.json')
    })
  ]
  // ----ここまで追加---
};
```

```sh: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がロードできているか（ブラウザのコンソールに出力されているか）を確認。

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

![スクリーンショット 2016-05-27 11.31.06.png](https://qiita-image-store.s3.amazonaws.com/0/18346/4ffb4995-1e65-d9b9-c77e-00220371130a.png "スクリーンショット 2016-05-27 11.31.06.png")

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

## 実際どれくらい速くなるか？

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

```sh: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`から取得するようになる。

```js: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をバンドルより先に読みこめばよい。

```html
<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に寄せる等の用途に使えそう。