🔨

Webpackのビルドをhard-source-webpack-pluginで高速化する

7 min read

概要

hard-source-webpack-pluginのREADMEの内容を要約しながら、実際に挙動を試した話。

(本記事はWebpack 4.x 向けです。 5.xからはまたキャッシュ周りの仕組みが変わっているように思えるのでご注意ください)

version

ツール バージョン
node 12.16.1
webpack 4.42.1
hard-source-webpack-plugin 0.13.1

HardSourceWebpackPlugin について

HardSourceWebpackPlugin は、Webpackによるビルド時に中間キャッシュを生成するステップを挿入するWebpackプラグインの一種です。

基本的には大きなデメリットも無く、(2回目以降の) ビルドを爆速化させます。

手元のプロジェクトでの例を見ても明らか

ビルドA ビルドB
1回目 238.37s 58.59s
2回目 33.48s 9.68s

※ ビルドの構成などの状況によって前後する上、本来は何度か計測を繰り返すのですが、ここでは雰囲気を伝えるために簡略化してます。より正確なベンチマークはググれば結構出てきます

インストール

npm install --save-dev hard-source-webpack-plugin

or

yarn add -D hard-source-webpack-plugin

最小限の設定

プロジェクトごとの設定ファイルに、プラグインを追加するだけです。

const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')

module.exports = {
  // 中略
  plugins: [
    new HardSourceWebpackPlugin()
  ]
}

多くの場合はこのデフォルト設定のままで十二分なパフォーマンス改善が行なえます。

キャッシュはどこに作られるの?

キャッシュはデフォルトでは node_modules/.cache/hard-source/ ディレクトリ以下に生成されます。

$ ls node_modules/.cache/hard-source/
4593f5079a29bba4d66c63ef9393ae46c0211a35803057c351111111caab218b
5a4f37fd271884b781b633e7054262a3dae9361b80f41e33e11111193c0eb25e
b615739e574ec28e0417ed832d15e11fe2cd8a4db0eb9ff7b111111985ba02b3

HardSourceWebpackPluginでは複数種のキャッシュを出し分けることができるので、キャッシュごとのハッシュ値別にさらにディレクトリが掘られています。

さらに覗いていくとキャッシュらしきものが沢山あることが確認できます。(内容は相当省略してます)

$ tree node_modules/.cache/hard-source/4593f5079a29bba4d66c63ef9393ae46c0211a11111111c35d76b1e7caab218b/
node_modules/.cache/hard-source/4593f5079a29bba4d66c63ef9393ae46c0211a11111111c35d76b1e7caab218b/
├── assets
│   ├── 018183c747e9f6de4e13967e1111dfb4f9f2aee9
│   ├── 01e1e80fd5b28cfec3c5f26d1111e16baa7b2353
├── assets-parity
│   ├── log0000
│   ├── log0001
├── md5
│   ├── log0000
│   ├── log0001
├── missing-resolve
│   └── log0000
├── module
│   ├── log0000
│   ├── log0001
├── module-resolve
│   ├── log0000
│   ├── log0001
├── resolver
│   ├── log0000
│   ├── log0001
├── stamp
└── version

キャッシュ生成先のディレクトリは、必要に応じて変更することができます。

new HardSourceWebpackPlugin({
  cacheDirectory: path.resolve('.cache/[confighash]')
})

[confighash] は、cacheDirectoryで使用できるキーワードで、キャッシュ出し分け用のハッシュ値が埋め込まれます。

そのため、決め打ちしたディレクトリでも環境ごとにキャッシュを出し分けることができます。

$ ls .cache/
4593f5079a29bba4d66c63ef9393ae46c0211a35803057c351111111caab218b
5a4f37fd271884b781b633e7054262a3dae9361b80f41e33e11111193c0eb25e
b615739e574ec28e0417ed832d15e11fe2cd8a4db0eb9ff7b111111985ba02b3

どうやって環境ごとのキャッシュを出し分けてるの?

ここまでの例では、以下の3つのビルドコマンドを実行した際に、それぞれのキャッシュが個別に生成されていることを確認してきました。

$ yarn webpack --config webpack/production.js
$ yarn webpack --config webpack/development.js
$ yarn webpack --config webpack/test.js
$ ls node_modules/.cache/hard-source/
4593f5079a29bba4d66c63ef9393ae46c0211a35803057c351111111caab218b
5a4f37fd271884b781b633e7054262a3dae9361b80f41e33e11111193c0eb25e
b615739e574ec28e0417ed832d15e11fe2cd8a4db0eb9ff7b111111985ba02b3

このキャッシュの出し分けは、HardSourceWebpackPluginconfigHashオプションを用いて制御することができます。

configHashは、使用するWebpack設定を受け取り、文字列を戻す関数を指定しますが、デフォルトで以下のようになっています。これによって、環境ごとというよりは、Webpackの設定ごとにキャッシュを出し分けることができているわけです。

configHash: function(webpackConfig) {
  return require('node-object-hash')({sort: false}).hash(webpackConfig);
}

参考

> maker = require('node-object-hash')({sort: false})
> maker.hash({foo: 1, bar: 2})
'af5bf01015faa20b1f97e67c252a72a5fc0cccc358ee1c0e2223a09ce1b5984b'
> maker.hash({foo: 1, bar: 3})
'843347059a6d3d1c8642d555df782f5e2d94b2ab4a858c852614aff1d3b0ea84'

もちろんこの設定もカスタム可能で、極端な例ですが、以下のようにWebpack設定をガン無視して、日付文字列を戻すようにしてみましょう。

new HardSourceWebpackPlugin({
  configHash: function(webpackConfig) {
    return `${Number(new Date())}`
  }
})
$ yarn webpack --config webpack/production.js
$ yarn webpack --config webpack/development.js
$ yarn webpack --config webpack/test.js
$ ls node_modules/.cache/hard-source/
1592321847954  1592321998294  1592322063186

ビルドするたびに実行時刻と思われるディレクトリが掘られていることがわかります。
なお、ビルドするたびに現在時刻を元にキャッシュを探すため、これらのキャッシュは二度と参照されません。

後述の environmentHash もあるので、基本的にはデフォルトのまま変える必要はないと思います。

ビルド構成の変更を検知してキャッシュを置き換えたい

前項では、configHashのデフォルト設定によって、Webpackの設定ファイルに応じたキャッシュを出し分けられることがわかりました。

しかし、実際のビルド構成はWebpackの設定ファイルだけに依存するとは限らず、特にnpmモジュールの追加削除あるいは更新の影響を大きく受けることがあります。

そういった場合のために、HardSourceWebpackPluginにはenvironmentHashというオプションがあります。

environmentHashはデフォルトでは以下のような設定になっています。

environmentHash: {
  root: process.cwd(),
  directories: [],
  files: ['package-lock.json', 'yarn.lock'],
},

これは、ビルドがpackage-lock.json yarn.lock に依存していることを表します(通常はどちらかですが)

上記の設定が適用されている場合、configHashの値(≒Webpackの設定)が同一であっても、対象ファイルに変更があった場合はキャッシュを置き換えることを意味します。

簡単な例を示します。

一度キャッシュを生成します

$ yarn webpack --config webpack/test.js
$ ll node_modules/.cache/hard-source/
drwxr-xr-x 9 root root 4096 Jun 16 16:00 3ba0cf2386860ce2a3c53aa100d08a7b145e3c41e6becc58f108e3e9625c1111/

適当にパッケージを削除して依存関係を書き換えます

$ yarn remove prettier

再度ビルドすると、Webpackの設定は変わっていないのに、キャッシュが更新されていることがわかります

$ yarn webpack --config webpack/test.js
$ ls node_modules/.cache/hard-source/
drwxr-xr-x 9 root root 4096 Jun 16 16:05 3ba0cf2386860ce2a3c53aa100d08a7b145e3c41e6becc58f108e3e9625c1111/

デフォルト設定でも充分ですが、もう少しプロジェクトの構成に合わせてカスタマイズしてみましょう。

例えば弊社プロダクトではnpmでなくyarnを採用していること、babelの設定はbabel.config.jsに記述していることから、以下のように設定する必要があります。

new HardSourceWebpackPlugin({
  environmentHash: {
    root: process.cwd(),
    directories: [],
    files: ['babel.config.js', 'yarn.lock'],
  },
})

この辺りの設定を工夫すれば、だいたいのプロジェクトで良い感じにキャッシュの管理ができそうですね。

webpack-dev-server とあわせて使用したい

ここまでいくつかの詳細設定に触れてきましたが、多くのプロジェクトではHMRのためなどの理由で、webpack-dev-serverを開発用サーバとして使っていると思います。

HardSourceWebpackPluginwebpack-dev-server でも使えるのでしょうか?
答えはYesです。

とはいえ、差分ビルド時にはいささか癖があるので注意が必要です。

ちなみにconfigHashについてですが、こちらは同じWebpack設定を使用していても、静的ビルド時とwebpack-dev-server使用時では異なるキャッシュが生成されます。これはwebpack-dev-serverがホットリロードのためのプラグインを動的に設定に追記しているためです。

わけわかんなくなったからキャッシュを削除したい

良い感じに設定を調整しても、キャッシュがぶっ壊れるときはぶっ壊れます。その場合は手動でキャッシュのディレクトリを丸ごと削除することで、確実にビルドをやり直すことができます。

下記はキャッシュ生成先がデフォルトの場合なので、変更している場合は読み替えてください。

$ rm -rf node_modules/.cache/hard-source/