KDE BLOG

バイブス

【webpack速習】vol.8: Code Splitting

下記ページの要点をまとめていきます。

webpack.js.org

  • コード分割(Code Splitting)は、webpackの最も魅力的な機能の1つ
  • この機能を使用すると、コードをさまざまなバンドルに分割して、それらをオンデマンド(注文対応)または並行して読み込むことができる
  • より小さなバンドルを作成し、リソース負荷の優先順位付けを制御するために使用できる。これを正しく使用すると、ロード時間に大きなプラス影響を与える可能性がある
  • 利用可能なコード分割には3つの一般的なアプローチがある
    • Entry Points: エントリ設定を使用してコードを手動で分割する
    • Prevent Duplication: SplitChunksPlugin を使用してチャンクを重複排除および分割する
    • Dynamic Imports: モジュール内のインライン関数呼び出しを介してコードを分割する

Entry Points

これは、コードを分割する最も簡単で直感的な方法である。
しかしながら、それはより手動で、いくつかの落とし穴がある。 メインのバンドルから他のモジュールを分割する方法を見てみる。

ディレクトリ構成

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

▼ src/another-module.js(新規作成)

import _ from 'lodash';

console.log(
  _.join(['Another', 'module', 'loaded!'], ' ')
);

▼ src/index.js

import _ from 'lodash';

function component() {
  var element = document.createElement('div');
  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  return element;
}

document.body.appendChild(component());

▼ webpack.config.js

const path = require('path');

module.exports = {
  mode: 'development',
  entry: {
    index: './src/index.js',
+   another: './src/another-module.js'
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

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

...
            Asset     Size   Chunks             Chunk Names
another.bundle.js  550 KiB  another  [emitted]  another
  index.bundle.js  550 KiB    index  [emitted]  index
Entrypoint index = index.bundle.js
Entrypoint another = another.bundle.js
...

エントリーポイントのファイル間に重複したモジュールがある場合(今回の場合は lodash)、それぞれのバンドルに含まれてしまっているのが問題。
この問題を解決するために SplitChunksPlugin を使う。

Prevent Duplication(重複を防ぐ)

SplitChunksPlugin を使うと、共通の依存関係(重複モジュール)を、既存のエントリーファイルまたは別の新しいファイルに抽出できる。
上記を例に、lodashの重複を排除してみる。
CommonsChunksPlugin はwebpack v4 legatoから削除された。

▼ webpack.config.js

module.exports = {
  // 略
+  optimization: {
+    splitChunks: {
+      chunks: 'all'
+    }
+  }
}

これでビルドすると出力結果は下記になる。

...
                          Asset      Size                 Chunks             Chunk Names
              another.bundle.js  5.95 KiB                another  [emitted]  another
                index.bundle.js  5.89 KiB                  index  [emitted]  index
vendors~another~index.bundle.js   547 KiB  vendors~another~index  [emitted]  vendors~another~index
Entrypoint index = vendors~another~index.bundle.js index.bundle.js
Entrypoint another = vendors~another~index.bundle.js another.bundle.js
...

index.bundle.js と another.bundle.js から lodash が削除され、新しくできた vendors~another~index.bunlde.js に lodash が抽出されているのが分かる。

※より詳しい SplitChunksPlugin の使い方は下記を参照
SplitChunksPlugin | webpack

code Splittingするためにその他にも便利なプラグインやローダーがコミュニティから提供されている。

  • mini-css-extract-plugin: アプリケーションから CSSを分割するのに役立つ
  • bundle-loader: コードを分割し、結果のバンドルを遅延ロードするために使用される
  • promise-loader: bundle-loader と似ているが promise を利用する

Dynamic Imports(動的インポート)

動的な code splitting に関しては、webpackでは2つの類似した方法がサポートされている。

  1. 【推奨】dynamic imports のためのECMAScript proposal に準拠した import() 構文を使用
  2. 【レガシー】require.ensure を使用

ここでは最初の dynamic import の使い方を見てみる。

import() 呼び出しは内部的に promise を使用しているため、古いブラウザで import() を使用する場合は、es6-promisepromise-polyfill などの polyfill を使用する。

▼ webpack.config.js

module.exports = {
  entry: {
    index: './src/index.js',
-  another: './src/another-module.js'
  },
  output: {
    filename: '[name].bunlde.js',
+  chunkFilename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
-  optimization: {
-    splitChunks: {
-      chunks: 'all'
-    }
-  }
  // 略
}

エントリーでないチャンクファイルの名前を chunkFilename プロパティに設定する。
chunkFilename の詳細については、下記参照。
Output | webpack

では lodash を静的に import する代わりに dynamic import を使用してチャンクを分離する。

▼ src/index.js

- import _ from 'lodash';
-
- function component() {
+ function getComponent() {
-   var element = document.createElement('div');
-
-   // Lodash, now imported by this script
-   element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+   return import(/* webpackChunkName: "lodash" */ 'lodash').then(({ default: _ }) => {
+     var element = document.createElement('div');
+     element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+     return element;
+   }).catch(error => 'An error occurred while loading the component');
  }

- document.body.appendChild(component());
+ getComponent().then(component => {
+   document.body.appendChild(component);
+ })

default が必要なのは、webpack 4以降、CommonJSモジュールを import する際に、import が module.exports の値に解決されなくなるため。代わりに、CommonJSモジュール用の人工名前空間オブジェクトが作成される。詳細は下記。
https://medium.com/webpack/webpack-4-import-and-commonjs-d619d626b655

import(/* webpackChunkName: "lodash" */ 'lodash') の書き方に関して、コメント上で /* webpackChunkName: "lodash" */ と記述することで バンドルの名前は [id] .bundle.js ではなく lodash.bundle.js となる。

webpackChunkName と他の利用可能なオプションについての詳細は下記。
Module Methods | webpack

これでビルド実行すると下記のようになる。

...
                   Asset      Size          Chunks             Chunk Names
         index.bundle.js  7.88 KiB           index  [emitted]  index
vendors~lodash.bundle.js   547 KiB  vendors~lodash  [emitted]  vendors~lodash
Entrypoint index = index.bundle.js
...

htmlからは、index.bundle.js を読み込むだけで良い。

async function を使う

async function を使うことでもっとすっきりと書くことができる。
ただし、使用には Babel や Syntax Dynamic Import Babel Plugin のようなプリプロセッサを使用する必要がある。

▼ src/index.js

async function getComponent () {
  const element = document.getElementBy('div');
  const { default: _ } = await import(/* webpackChunkName: 'lodash' */, 'lodash');
  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
  return element;
}

// 略

Prefetching/Preloading modules

webpack 4.6.0以降では、プリフェッチとプリロードのサポートが追加されている。

ここの節に関しては、現在すぐには使用しなそうなのでこの場ではスキップ。

Bundle Analysis(バンドル分析)

コードの分割を開始したら、出力を分析してモジュールがどこで終了したかを確認すると良い。
公式の分析ツールは、最初に分析を始めるのに良いツール。
他にもコミュニティでサポートされているものがある。

  • webpack-chart
  • webpack-visualizer
  • webpack-bundle-analyzer
  • webpack bundle optimize helper: