webpack@5で入るPersistent Cachingについて
webpackはin-memoryのみで今まで永続的なキャッシュを実装していませんでした。理由としては、パフォーマンスよりも安全性を優先していたためです。
cache-loaderを使ったことがある人はわかるかもしれませんが、確かに速くなる一方、安全性が損なわれているのは事実です。
この機能は、webpackはデフォルトでファイルキャッシュをオンにはしませんがそれでもビルドの速度を上げたい場合に使う機能です。
以下がデフォルトの挙動となります。
mode | cache |
---|---|
development | memory |
production | false |
実際に使うときの設定
結論ですが、webpack.config.jsへ以下のように書くことが推奨されます。
module.exports = { cache: { type: 'filesystem', buildDependencies: { config: [__filename] } } };
あとは、各コードの設定に依存するためversion
等の追加が必要になる可能性があります。
ドキュメント
仕組み
ファイルキャッシュでは以下のようにデフォルトではnode_modules/.cache/webpack
というディレクトリにスナップショットを生成します。
(๑˃̵ᴗ˂̵)و ~/D/w/node_modules ᐅ tree .cache .cache └── webpack └── default-production ├── 0.pack <-- 生成済みコードの記録 └── index.pack <-- 依存ファイル等の記録 2 directories, 2 files
このようにシリアル化されたデータを保存します。
MD4ハッシュアルゴリズムを用いたetagを識別子とし、それと一致したものをwebpackは使用します。(実装が知りたい人はcreateHash.jsとPackFileCacheStrategy.jsを読んでください)
本番環境とbuildDependenciesには、 timestamp + hash モードがデフォルトで適応されます。
使うユーザーは snapshot
のオプションを設定することはないと思うので、この記事では割愛します。
https://webpack.js.org/configuration/other-options/#snapshot
webpackは、すべてのモジュールそれぞれに対し、compilation.fileDependencies
, compilation.contextDependencies
, compilation.missingDependencies
をトラッキングし、スナップショットを生成していきます。
余談ですが、この三点は、webpack@5からSortable Set
からSet
となり、並び替えが不可能となりましたのでプラグイン作者は気をつけてください。
つまり、特定ファイルを変更するとwebpackはそのファイルのキャッシュ(webpack内ではキャッシュエントリと呼ばれる)を無効化し、各種loaderを実行後にファイル解析を行い再生成を行います。また、bundle.jsのキャッシュエントリを無効化し、このファイルを再生成する可能性があります。
例えば、依存ファイルを変更すると以下のように変更されます。
(๑˃̵ᴗ˂̵)و ~/D/w/node_modules ᐅ tree .cache .cache └── webpack └── default-production ├── 0.pack ├── 1.pack ├── index.pack └── index.pack.old 2 directories, 4 files
1の方が新しくなり、スナップショットが追加されました。
キャッシュエントリが無効化されるケース
以下の場合にキャッシュエントリが無効化されます。
- 監視下のファイルが変更されたとき
- 設定を変更したとき
- webpack.config.jsの
cache
等の設定変更
- webpack.config.jsの
- loaderかpluginがパッケージアップデートされたとき
- 依存関係(node_modules)がパッケージアップデートされたとき
- cliからビルドに影響のある値を送ったとき
--optimization-minimize
, コードで判断できないもの
- カスタムなビルドスクリプトが変更されたとき
cache.version
,cache.name
,cache.buildDependencies
ビルド結果を変更する可能性のあるものはキャッシュエントリが無効化されます。
例えば、--optimization-minimize
を渡せばビルド結果には影響されます。しかし入力されたソースコードの変更だけではこれを検知できませんが、キャッシュはこれを考慮する必要があります。 webpackではそれに対して、cache.version
, cache.name
, cache.buildDependencies
を使い処理をしますが、これを自動的に認識するのは難しいため影響が生じたときに再構築する必要が出てきます。(かなり安全性を重視しています)
オプション
最低限のものだけ説明します。
type
memory
とfilesystem
が存在し、どちらを選択することができます。
buildDependencies
cache.buildDependencies
には、ビルドにおけるコード依存関係を追加します。
defaultWebpack
webpackのすべての依存関係を取得するために、デフォルトでwebpack/lib
となります。
この設定は基本的に設定する必要はないです。
module.exports = { cache: { type: 'filesystem', buildDependencies: { defaultWebpack: ['webpack/lib'], } } };
config
公式では、最新の設定(webpack.config.js, etc)とすべての依存を取得するために__ filename
を設定することが推奨されます。
このように書くことにより、設定とすべての依存関係を取得するようになります。
module.exports = { cache: { type: 'filesystem', buildDependencies: { config: [__filename] } } };
ディレクトリの場合、最も近いpackage.jsonの依存関係が分析され、ファイルの場合は、Node.jsのモジュールのキャッシュを見て、依存関係をwebpackは把握します。
注意点として、ディレクトリの場合は、必ずスラッシュで終わる必要があります(そうしないとファイルと識別されてしまう)
version
たとえ同じ内容でも、この値を変更することにより永続的キャッシュを無効化することができます。
ビルドの一部の依存において、表現できない場合が存在します。(e.g. DBから読み込まれた値、環境変数、コマンドラインで渡される値)
もしキャッシュがおかしい場合このオプションを確認・検討してください。
もしコードがdefinePlugin経由で環境変数を入れていてそれをバンドルに埋め込む場合、これはこの環境変数(e.g. gitのrevision)への依存があるので、バージョン名をこの値にし、キャッシュを無効化する必要があります。
module.exports = { cache: { type: 'filesystem', buildDependencies: { config: [__filename] } }, plugins: [ new webpack.DefinePlugin({ 'process.env.foo': JSON.stringify('foo') }) ] };
上記の場合はconfig
でこのwebpack.config.jsを監視下に置いており、そこでfoo
を定義しているため、この文字列を変更したらwebpackは検知できるため問題ないです。問題は、監視下でwebpackが変更されたか認知できない(ずっと末端まで変数等)です。
しかし、.env
を利用する場合は以下のようにversion
を指定しないとキャッシュは更新されません。
# .env VERSION=1.0
const webpack = require('webpack'); const { config } = require('dotenv'); config(); module.exports = { cache: { type: 'filesystem', buildDependencies: { config: [__filename] }, version: process.env.VERSION }, plugins: [ new webpack.DefinePlugin({ 'process.env.version': process.env.VERSION }) ] };
さてこれで問題になるのが、もしこれをしっかり設定しないと過去のスナップショットを参照するため生成結果も過去の状態になることです。これがwebpackが恐れていた問題です。この場合だと、version
を指定しない場合、出力は常に最初のビルド時のものとなり.envで書き換えしても反映されません。
つまり、cache.name
でも、キャッシュを無効化できるためコードに依存しますが導入の検討する可能性があります。
パフォーマンスの最適化
node_modulesのコードに対して、timestamp + hashで管理するとコストがかかりビルド速度が低下するためwebpackでは、package.json内のバージョンと名前を利用し評価しています。 なので、絶対にnode_modules内のコードを編集することは避けてください。
この最適化は、snapshot.managedPaths
のパスに適応され、デフォルトではwebpackがインストールされているnode_modulesとなります。yarn.pnpの場合、ファイルパスでハッシュを利用するため上記の最適化はyarnがカバーするため行われません。
d3を使った場合のパフォーマンス測定
// index.js import * as d3 from 'd3'; import 'foo.js';
// foo.js console.log('foo');
// webpack.config.js module.exports = { cache: { type: 'filesystem', buildDependencies: { config: [__filename] } } };
最初はキャッシュがないため、webpack@4同様に以下の速度ぐらいとなります。
asset main.js 36.3 KiB [emitted] [minimized] (name: main) orphan modules 584 KiB [orphan] 554 modules cacheable modules 117 KiB ./src/index.js + 103 modules 117 KiB [built] [code generated] ./src/foo.js 21 bytes [built] [code generated] webpack 5.0.0-rc.2 compiled successfully in 1836 ms
もう一度、何も変更せずにビルドを行ってみます。
asset main.js 36.3 KiB [compared for emit] [minimized] (name: main) cached modules 700 KiB [cached] 556 modules webpack 5.0.0-rc.2 compiled successfully in 429 ms
前回と同様なのですべてキャッシュが使われていることがわかり、約4.5倍程度早くなったことが確認できます。
それでは、foo.jsの中身を変更します。
asset main.js 36.3 KiB [emitted] [minimized] (name: main) cached modules 700 KiB [cached] 555 modules ./src/foo.js 18 bytes [built] [code generated] webpack 5.0.0-rc.2 compiled successfully in 1228 ms
foo.jsのコードのみが再度生成され、foo.jsのスナップショットが更新されました。index.jsを含め更新されたわけではないため速度はフルビルドの時よりも速くなります。
再度、何も変更せずにビルドを行います。
asset main.js 36.3 KiB [compared for emit] [minimized] (name: main) cached modules 700 KiB [cached] 556 modules webpack 5.0.0-rc.2 compiled successfully in 416 ms
すべてのキャッシュが利用され変更がないため、400ms台で落ち着きました。
さいごに
webpack5への機能追加として、一番投票率が多かったのがこの永続的キャッシュという機能でした。
この機能は、開発時に大いに役に立つと思います。もし、webpackのビルド時間に不満がある人はこの機能を試してみると良いかなと思います。