KDE BLOG

Webデザインやコーディングについて書いています

【webpack速習】vol.10: Caching

下記ページをざっくりまとめます。

webpack.js.org

  • ブラウザはキャッシュにより、不要なトラフィックを避けサイトの読み込みが高速になることがあるが新しいコードを取得するに困ることがある
  • 本ページでは、内容が変更されていない限り、Webpackのコンパイルによって生成されたファイルがキャッシュされたままになることを保証するために必要な設定に焦点を当てる

Output Filenames

  • output.filename の置換設定を使うことで出力ファイルの名前を定義できる
  • webpackは、置換と呼ばれる[ ]で囲まれた文字列を使ってファイル名をテンプレート化する方法を提供している
  • [contenthash] 置換は、アセットのコンテンツに基づいて一意のハッシュを追加する
    • アセットのコンテンツが変わると [contenthash] も変わる

実際に試してみる。

ディレクトリ構成

project
|- package.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
|- /node_modules

▼ webpack.config.js

  const path = require('path');
  const CleanWebpackPlugin = require('clean-webpack-plugin');
  const HtmlWebpackPlugin = require('html-webpack-plugin');

  module.exports = {
    entry: './src/index.js',
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
-       title: 'Output Management'
+       title: 'Caching'
      })
    ],
    output: {
-     filename: 'bundle.js',
+     filename: '[name].[contenthash].js',
      path: path.resolve(__dirname, 'dist')
    }
  };

これでビルドを実行すると下記のような結果が出力される。

                       Asset       Size  Chunks                    Chunk Names
main.7e2c49a622975ebd9b7e.js     544 kB       0  [emitted]  [big]  main
                  index.html  197 bytes          [emitted]

中身を何も変えずに再ビルドすると、ファイル名は変わらず再度出力されるが、少しでも中身を変えると [contenthash] 部分が変更される。
これは、webpackのエントリチャンクに特定の定型句、具体的にはランタイムとマニフェストが含まれているため。

※Webpackのバージョンによって出力が異なる場合がある。 安全のために下記手順を推奨。

Extracting Boilerplate

code splitting で学んだように、SplitChunksPlugin を使ってモジュールを別々のバンドルに分割することができる。

webpackは、optimize.runtimeChunk オプションを使用してランタイムコードを別々のチャンクに分割するための最適化機能を提供している。
すべてのチャンクに対して単一のランタイムバンドルを作成するには、single に設定する。

▼ webpack.config.js

// 略
module.exports = {
   // 略
+  optimization: {
+    runtimeChunk: 'single'
+  }
}

ビルド実行結果

Hash: 82c9c385607b2150fab2
Version: webpack 4.12.0
Time: 3027ms
                          Asset       Size  Chunks             Chunk Names
runtime.cc17ae2a94ec771e9221.js   1.42 KiB       0  [emitted]  runtime
   main.e81de2cf758ada72f306.js   69.5 KiB       1  [emitted]  main
                     index.html  275 bytes          [emitted]
[1] (webpack)/buildin/module.js 497 bytes {1} [built]
[2] (webpack)/buildin/global.js 489 bytes {1} [built]
[3] ./src/index.js 309 bytes {1} [built]
    + 1 hidden module

lodashreact などのサードパーティ製のライブラリは、アプリケーションコードよりも変更される可能性は低いので、別のチャンクに分けることを推奨している。
それによりクライアントからのリクエストを少なくすることができる。
これは、SplitChunksPlugin の例2に示されている SplitChunksPlugincacheGroups オプションを使用して実行できる。

▼ webpack.config.js

// 略
module.exports = {
   // 略
  optimization: {
    runtimeChunk: 'single',
+  splitChunks: {
+      cacheGroups: {
+        vendor: {
+          test: /[\\/]node_modules[\\/]/,
+          name: 'vendors',
+          chunks: 'all'
+        }
+      }
+    }
  }
}

ビルド実行結果

                          Asset       Size  Chunks             Chunk Names
runtime.cc17ae2a94ec771e9221.js   1.42 KiB       0  [emitted]  runtime
vendors.a42c3ca0d742766d7a28.js   69.4 KiB       1  [emitted]  vendors
   main.abf44fedb7d11d4312d7.js  240 bytes       2  [emitted]  main
                     index.html  353 bytes          [emitted]

メインバンドルに node_modules ディレクトリのベンダーコードが含まれておらず、サイズが240バイトに減少していることがわかる。

Module Identifiers

print.js をプロジェクトに追加する

ディレクトリ構成

project
|- package.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
+ |- print.js
|- /node_modules

▼ src/print.js

export default text => {
  console.log(text);
};

▼ src/index.js

import _ from 'lodash';
+ import print from './print';

function component() {
  const element = document.createElement('div');
  const button = document.createElement('button');
  const br = document.createElement('br');

  button.innerHTML = 'Click me and look at the console!!';
  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  element.appendChild(br);
  element.appendChild(button);

+  button.onclick = print.bind(null, 'Hello, webpack!!');

  return element;
}

document.body.appendChild(component());

これでビルドすると、main バンドルがだけが更新されると思いきや、実際には、下記のように3つすべてが更新される

                           Asset       Size  Chunks                    Chunk Names
  runtime.1400d5af64fc1b7b3a45.js    5.85 kB      0  [emitted]         runtime
  vendor.a7561fb0e9a071baadb9.js     541 kB       1  [emitted]  [big]  vendor
    main.b746e3eb72875af2caa9.js    1.22 kB       2  [emitted]         main
                      index.html  352 bytes          [emitted]

これは、各module.idがデフォルトで順序の解決に基づいて増分されるため。
解決の順序が変わると、IDも変わる。
まとめると下記となる。

  1. main バンドルは内容が更新されたため変更された
  2. vender バンドルは module.id が更新されたため変更された
  3. manifest バンドル(≒ runtime バンドル)は新しいモジュールへの参照を含むようになったため変更された

1と3は予想できる内容である。
vendor のハッシュに関しては何とかしたいが、これを解決するために使用できるプラグインが2つある。

  • NamedModulesPlugin
    数値の識別子ではなくモジュールへのパスを使用する。開発時に読みやすく出力するのに役立つが、実行には少し時間がかかる
  • HashedModuleIdsPlugin
    プロダクションビルドに推奨される

今回は2番目の HashedModuleIdsPlugin を使用。
webpack.config.js でpluginsに追加する。

▼ webpack.config.js

+ const webpack = require('webpack');
// 略
module.exports = {
    entry: './src/index.js',
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
        title: 'Caching'
      }),
+      new webpack.HashedModuleIdsPlugin()
    ],
    // 略

これにより、vendor のハッシュは変更せずに保たれるようになる。

キャッシュに関しては複雑になる可能性があるが、そのメリットは大きい。
詳細に関しては下記のissueを参考。
Guides - Explain Hash Changes in Caching Guide · Issue #652 · webpack/webpack.js.org · GitHub