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バンドルの作り方
webpack.DllPlugin
を使って、DLLバンドルと、そのDLLバンドルに含まれるモジュールの情報が書かれたmanifestファイル(jsonファイル)を生成する。
サンプル
例として、react
とreact-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
という名前にする。
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バンドルの使い方
作った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を書く。
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。
$ ./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を追加してビルドする。
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.01KB。めっちゃ軽くなった。
実際に下記のようなhtmlを書いて、react, react-domがロードできているか(ブラウザのコンソールに出力されているか)を確認。
<body>
<script src="dist/vendor.dll.js"></script>
<script src="dist/dll-user.bundle.js"></script>
</body>
ちゃんと出力されている。うえーい。
実際どれくらい速くなるか?
react, fluxibleで作っているツールで試してみた。DLLバンドルにはreact, react-dom, fluxibleなど、npmでインストールしたモジュールを全部突っ込む。
// 導入前
$ 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
から取得するようになる。
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に寄せる等の用途に使えそう。