Edited at

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