webpackのビルド高速化の効果を測ってみた


[webpack](http://webpack.github.io/)によるビルドを高速化するための設定がいくつかあるのでそれらの効果を(雑にですが)測ってみました。
なお、ビルド時間の測定には手前味噌ですが、[React.js](https://facebook.github.io/react/)用のGridコンポーネントである[Griddle](http://griddlegriddle.github.io/Griddle)をBootstrapデザインにする[griddle-react-bootstrap](https://github.com/wadahiro/griddle-react-bootstrap)の[examplesアプリ](https://github.com/wadahiro/griddle-react-bootstrap/tree/master/examples)を使用しています(動いている物は[こちらから見れます](http://wadahiro.github.io/griddle-react-bootstrap/))。使用ライブラリは下記の`package.json`の通り。TypeScriptで書いているので、webpack + [ts-loader](https://github.com/TypeStrong/ts-loader)でビルドしています。[Babel](http://babeljs.io/)は使用していませんが傾向はたぶん同じになるかと思います。

```js:package.json
  "devDependencies": {
    "cross-env": "^1.0.7",
    "ts-loader": "^0.7.2",
    "typescript": "^1.8.2",
    "webpack": "^1.12.11"
  },
  "typings": "lib/index.d.ts",
  "dependencies": {
    "griddle-react-bootstrap": "^0.4.2",
    "griddle-react": "^0.3.1",
    "react": "^0.14.6",
    "react-bootstrap": "^0.28.2",
    "react-dom": "^0.14.6",
    "underscore": "^1.8.3"
  }
```


# webpackのconfig

測定に使用した設定パターンについて説明。A～Cがありますが、Aを基本として各設定を行っていきます。Bの`devtool`設定はB1～B5と細かく分けて試してみました。

なお、webpackのconfigにインクリメンタルビルドに効果がありそうな[cache設定](https://webpack.github.io/docs/configuration.html#cache)がありますが、watchモードの場合はデフォルト有効になるとのことですが、念のため`true`を明示的に設定しています。

## A 基準となる設定
* `cache: true`
* `devtool: '#source-map'`
* 使用プラグイン
 * watchモード時: なし
 * プロダクションビルド時: 下記の通り

```js
    new webpack.optimize.OccurenceOrderPlugin(true),
    new webpack.optimize.DedupePlugin(),
    new webpack.optimize.UglifyJsPlugin({ output: { comments: false } })
```

## B `devtool` を変更してSourceMapの方式を変更

[devtoolの説明](http://webpack.github.io/docs/configuration.html#devtool)や[BUILD PERFORMANCE](http://webpack.github.io/docs/build-performance.html)を見ると、SourceMapの方式によってビルドパフォーマンスに大きく影響があるとのことなのでこれを変えてみます。


### B1 `devtool: '#inline-source-map'`に変更

```js
{
    ...
    devtool: '#inline-source-map'
}
```


### B2 `devtool: '#eval-source-map'`に変更

```js
{
    ...
    devtool: '#eval-source-map'
}
```

### B3 `devtool: '#eval-cheap-module-source-map'`に変更

```js
{
    ...
    devtool: '#eval-cheap-module-source-map'
}
```

### B4 `devtool: '#eval-cheap-source-map'`に変更

```js
{
    ...
    devtool: '#eval-cheap-source-map'
}
```

### B5 `devtool: '#eval'`に変更

```js
{
    ...
    devtool: '#eval'
}
```

## C: `externals` で外部ライブラリを対象外に

webpackの場合、configに`externals`を設定することで、ビルド時に`node_modules`以下から読んでいた外部ライブラリをバンドル対象外にできます。ビルド時にパースしなくなるので大きなパフォーマンス向上が望めます。
通常、アプリケーション以外のサードパーティ製のライブラリには変更が入らないことが多いので、できるなら`externals`を使って分離しておくと良さそうです(ライブラリ側が用意してくれている必要あり)。
また、サードパーティ製のライブラリはCDNから読み込むようにする場合もこの設定になります。

今回の測定では、使用しているライブラリのうち下記の通り5つを設定して測定しました。

```js
{
    ...
    externals: {
        "react": "React",
        "react-dom": "ReactDOM",
        "react-bootstrap": "ReactBootstrap",
        "griddle-react": "Griddle",
        "underscore": "_"
    },
    ...
}
```

なお、実際に動かす場合はHTMLなどでこれらのライブラリを別途手動で読ませる必要があるので注意。また、ローカル環境だと下記のように`node_modules`から読むことはできますが、実際のサーバにデプロイする際はちょっと工夫が必要になります。これについてはまた別の記事を書きたいと思います。

```html:例
        <script type="text/javascript" src="../node_modules/react/dist/react-with-addons.min.js"></script>
        <script type="text/javascript" src="../node_modules/react-dom/dist/react-dom.min.js"></script>
        <script type="text/javascript" src="../node_modules/react-bootstrap/dist/react-bootstrap.min.js"></script>
        <script type="text/javascript" src="../node_modules/underscore/underscore-min.js"></script>
        <script type="text/javascript" src="../node_modules/griddle-react/build/griddle.js"></script>
```


# 測定ケース

* watchモードでビルド (`webpack -w`)
* watchモードでビルド後のインクリメンタルビルド (1ファイル更新)
* プロダクションビルド

のケースを測定。インクリメンタルビルドは2回目以降は速くなる傾向があるので複数回実行させました。
なお、誤差が結構(数百ms)ありブレるので、何回か試して真ん中くらいの値を雑に採用しています。まぁあくまで目安ということで。

# 結果
単位はmsです。

| 測定ケース                  | A        | A+B1     | A+B2     | A+B3     | A+B4     | A+B5     | A+C     | A+B1+C |
|:--------------------------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:-------:|:-------:|
|watchモードでビルド          |      4667| 4634     | 4256     | 3722     | 3640     | 3567     | 1947    | 1930    |
|インクリメンタルビルド(1回目) |      2438| 2293     | 2012     | 1499     | 1633     | 1211     | 937     | 902     |
|インクリメンタルビルド(2回目以降)|   1863| 1461     | 880      | 903      | 863      | 875      | 707     | 689     |
|プロダクションビルド         |     17992| 16046    | 6206     | 5699     | 5647     | 4981      | 2347    | 2318     |


# 考察

* ビルド速度は開発時しか基本気にしないのであれば、Reactのような大きなライブラリも含めて丸ごとビルドしてもインクリメンタルビルドであればそこそこ速い。
* SourceMapと`externals`設定の効果が大きい。特に、大きなライブラリを利用するケースだとかなり効いてくる。
* ただしSourceMapはデバッグ時にないと基本困るので、`devtool`設定を変えれば速くはなるが精度が落ちないレベルの物を使用した方が良いと思う。特にReactでJSXを使う場合は、`eval-cheap-source-map`や`eval`だとJSに変換された物にマップされてしまいJSXでデバッグできないので注意。また、性能とは別件なのですが、Chromeで`eval`系だとブレークポイントが効かないという問題に遭遇しているため、個人的には`inline-source-map`を使っています。
* たくさん外部ライブラリを使う場合は`externals`を使って対象外にすることを検討したほうが良さそう。

# 補足

今回はBabelは使っていませんが、使用する場合は[babel-loaderのcacheDirectory設定](https://github.com/babel/babel-loader#babel-loader-is-slow)を設定すると良さそうです。