コード分割

コード分割はwebpackの最も魅力的な機能の1つです。この機能を使用すると、コードをさまざまなバンドルに分割し、オンデマンドまたは並行してロードできます。これは、より小さなバンドルを実現し、リソースロードの優先順位を制御するために使用でき、正しく使用すれば、ロード時間に大きな影響を与える可能性があります。

利用可能なコード分割には、3つの一般的なアプローチがあります。

  • エントリポイントentry設定を使用して、手動でコードを分割します。
  • 重複を防ぐエントリの依存関係またはSplitChunksPluginを使用して、チャンクを重複排除および分割します。
  • 動的インポート:モジュール内のインライン関数呼び出しを介してコードを分割します。

エントリポイント

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

プロジェクト

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

another-module.js

import _ from 'lodash';

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

webpack.config.js

 const path = require('path');

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

これにより、次のビルド結果が得られます。

...
[webpack-cli] Compilation finished
asset index.bundle.js 553 KiB [emitted] (name: index)
asset another.bundle.js 553 KiB [emitted] (name: another)
runtime modules 2.49 KiB 12 modules
cacheable modules 530 KiB
  ./src/index.js 257 bytes [built] [code generated]
  ./src/another-module.js 84 bytes [built] [code generated]
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 245 ms

前述のように、このアプローチにはいくつかの落とし穴があります。

  • エントリチャンク間に重複したモジュールがある場合、両方のバンドルに含まれます。
  • 柔軟性がなく、コアアプリケーションロジックを使用して動的にコードを分割するために使用することはできません。

これらの2つのポイントの最初のポイントは、./src/index.js内でもlodashがインポートされており、両方のバンドルで重複するため、確かに例の問題です。次のセクションでこの重複を削除しましょう。

重複の防止

エントリーの依存関係

dependOn オプションを使用すると、チャンク間でモジュールを共有できます。

webpack.config.js

 const path = require('path');

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

単一のHTMLページで複数のエントリーポイントを使用する場合、optimization.runtimeChunk: 'single' も必要です。そうしないと、こちらで説明されている問題が発生する可能性があります。

webpack.config.js

 const path = require('path');

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

そして、これがビルドの結果です。

...
[webpack-cli] Compilation finished
asset shared.bundle.js 549 KiB [compared for emit] (name: shared)
asset runtime.bundle.js 7.79 KiB [compared for emit] (name: runtime)
asset index.bundle.js 1.77 KiB [compared for emit] (name: index)
asset another.bundle.js 1.65 KiB [compared for emit] (name: another)
Entrypoint index 1.77 KiB = index.bundle.js
Entrypoint another 1.65 KiB = another.bundle.js
Entrypoint shared 557 KiB = runtime.bundle.js 7.79 KiB shared.bundle.js 549 KiB
runtime modules 3.76 KiB 7 modules
cacheable modules 530 KiB
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
  ./src/another-module.js 84 bytes [built] [code generated]
  ./src/index.js 257 bytes [built] [code generated]
webpack 5.4.0 compiled successfully in 249 ms

ご覧のとおり、shared.bundle.jsindex.bundle.jsanother.bundle.js の他に、もう1つ runtime.bundle.js ファイルが生成されています。

webpackではページごとに複数のエントリーポイントを使用できますが、可能な限り、複数のインポートを持つエントリーポイント (entry: { page: ['./analytics', './app'] }) を優先して避ける必要があります。これにより、async スクリプトタグを使用する場合に、より良い最適化と一貫した実行順序が得られます。

SplitChunksPlugin

SplitChunksPlugin を使用すると、共通の依存関係を既存のエントリーチャンクまたは完全に新しいチャンクに抽出できます。これを使用して、前の例の lodash の依存関係を重複排除してみましょう。

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'),
    },
+   optimization: {
+     splitChunks: {
+       chunks: 'all',
+     },
+   },
  };

optimization.splitChunks 設定オプションを適用すると、index.bundle.jsanother.bundle.js から重複した依存関係が削除されているはずです。プラグインは、lodash を別のチャンクに分離したことを検出し、メインバンドルから不要なものを削除します。npm run build を実行して、動作するかどうかを確認しましょう。

...
[webpack-cli] Compilation finished
asset vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB [compared for emit] (id hint: vendors)
asset index.bundle.js 8.92 KiB [compared for emit] (name: index)
asset another.bundle.js 8.8 KiB [compared for emit] (name: another)
Entrypoint index 558 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB index.bundle.js 8.92 KiB
Entrypoint another 558 KiB = vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB another.bundle.js 8.8 KiB
runtime modules 7.64 KiB 14 modules
cacheable modules 530 KiB
  ./src/index.js 257 bytes [built] [code generated]
  ./src/another-module.js 84 bytes [built] [code generated]
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 241 ms

以下に、コミュニティが提供する、コード分割に役立つ他のプラグインとローダーをいくつか示します。

動的インポート

webpackでは、動的なコード分割に関して2つの類似した手法がサポートされています。最初で推奨されるアプローチは、動的インポートに関するECMAScriptプロポーザルに準拠するimport()構文を使用することです。レガシーなwebpack固有のアプローチは、require.ensureを使用することです。まず、この2つのアプローチの最初のものを試してみましょう。

始める前に、上記の例の余分なentryoptimization.splitChunks を設定から削除しましょう。これは次のデモンストレーションには必要ありません。

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'),
   },
-  optimization: {
-    splitChunks: {
-      chunks: 'all',
-    },
-  },
 };

また、プロジェクトを更新して、不要になったファイルを削除します。

プロジェクト

webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
  |- index.js
- |- another-module.js
|- /node_modules

さて、lodash を静的にインポートする代わりに、動的インポートを使用してチャンクを分割します。

src/index.js

-import _ from 'lodash';
-
-function component() {
+function getComponent() {
-  const element = document.createElement('div');

-  // Lodash, now imported by this script
-  element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+  return import('lodash')
+    .then(({ default: _ }) => {
+      const element = document.createElement('div');
+
+      element.innerHTML = _.join(['Hello', 'webpack'], ' ');

-  return element;
+      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モジュールをインポートするときに、インポートが module.exports の値に解決されなくなり、代わりにCommonJSモジュール用の人工的な名前空間オブジェクトが作成されるためです。この理由の詳細については、webpack 4: import() and CommonJs をお読みください。

webpackを実行して、lodash が別のバンドルに分割されていることを確認しましょう。

...
[webpack-cli] Compilation finished
asset vendors-node_modules_lodash_lodash_js.bundle.js 549 KiB [compared for emit] (id hint: vendors)
asset index.bundle.js 13.5 KiB [compared for emit] (name: index)
runtime modules 7.37 KiB 11 modules
cacheable modules 530 KiB
  ./src/index.js 434 bytes [built] [code generated]
  ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
webpack 5.4.0 compiled successfully in 268 ms

import() はPromiseを返すため、async関数で使用できます。以下にコードを簡略化する方法を示します。

src/index.js

-function getComponent() {
+async function getComponent() {
+  const element = document.createElement('div');
+  const { default: _ } = await import('lodash');

-  return import('lodash')
-    .then(({ default: _ }) => {
-      const element = document.createElement('div');
+  element.innerHTML = _.join(['Hello', 'webpack'], ' ');

-      element.innerHTML = _.join(['Hello', 'webpack'], ' ');
-
-      return element;
-    })
-    .catch((error) => 'An error occurred while loading the component');
+  return element;
 }

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

モジュールのプリフェッチ/プリロード

Webpack 4.6.0以降では、プリフェッチとプリロードのサポートが追加されました。

インポートを宣言する際にこれらのインラインディレクティブを使用すると、webpackは「リソースヒント」を出力します。これは、ブラウザに以下を伝えます。

  • prefetch: リソースは将来のナビゲーションで必要になる可能性があります
  • preload: リソースは現在のナビゲーションでも必要になります

この例として、HomePageコンポーネントがあり、LoginButtonコンポーネントをレンダリングし、クリックされるとオンデマンドでLoginModalコンポーネントをロードする場合を考えます。

LoginButton.js

//...
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');

これにより、<link rel="prefetch" href="login-modal-chunk.js"> がページのheadに追加され、ブラウザにアイドル時間に login-modal-chunk.js ファイルをプリフェッチするように指示します。

プリロードディレクティブには、プリフェッチと比較して多くの違いがあります。

  • プリロードされたチャンクは、親チャンクと並行してロードを開始します。プリフェッチされたチャンクは、親チャンクのロードが完了した後に開始します。
  • プリロードされたチャンクは中程度の優先度を持ち、即座にダウンロードされます。プリフェッチされたチャンクは、ブラウザがアイドル状態のときにダウンロードされます。
  • プリロードされたチャンクは、親チャンクによって即座に要求される必要があります。プリフェッチされたチャンクは、将来いつでも使用できます。
  • ブラウザのサポートが異なります。

この例として、常に別のチャンクにあるべき大きなライブラリに依存するComponentがある場合を考えます。

ChartingLibraryという大きなライブラリを必要とするChartComponentというコンポーネントを想像してみましょう。レンダリング時にLoadingIndicatorを表示し、オンデマンドでChartingLibraryをインポートします。

ChartComponent.js

//...
import(/* webpackPreload: true */ 'ChartingLibrary');

ChartComponentを使用するページがリクエストされると、<link rel="preload">を介してcharting-library-chunkもリクエストされます。ページチャンクが小さく、より速く完了すると仮定すると、ページは、既にリクエストされたcharting-library-chunkが完了するまで、LoadingIndicatorとともに表示されます。これにより、2往復ではなく1往復で済むため、ロード時間が少し短縮されます。特に高レイテンシー環境ではそうです。

場合によっては、プリロードを独自に制御する必要がある場合があります。たとえば、動的インポートのプリロードは、非同期スクリプトを介して実行できます。これは、ストリーミングサーバーサイドレンダリングの場合に役立ちます。

const lazyComp = () =>
  import('DynamicComponent').catch((error) => {
    // Do something with the error.
    // For example, we can retry the request in case of any net error
  });

webpack自体がそのスクリプトのロードを開始する前にスクリプトのロードが失敗した場合(webpackは、そのスクリプトがページ上にない場合、そのコードをロードするためのスクリプトタグを作成します)、そのcatchハンドラーはchunkLoadTimeoutが経過するまで開始されません。この動作は予想外の場合があります。しかし、これは説明可能です。webpackはエラーをスローできません。webpackは、スクリプトが失敗したことを認識していないためです。webpackは、エラーが発生した直後にスクリプトにonerrorハンドラーを追加します。

このような問題を回避するには、エラーが発生した場合にスクリプトを削除する独自のonerrorハンドラーを追加できます。

<script
  src="https://example.com/dist/dynamicComponent.js"
  async
  onerror="this.remove()"
></script>

その場合、エラーになったスクリプトは削除されます。webpackは独自のスクリプトを作成し、エラーはタイムアウトなしで処理されます。

バンドル分析

コードの分割を開始すると、モジュールがどこに配置されたかを確認するために、出力を分析することが役立つ場合があります。公式の分析ツールは、開始するのに最適な場所です。他にも、コミュニティでサポートされているオプションがいくつかあります。

  • webpack-chart: webpackの統計情報用のインタラクティブな円グラフ。
  • webpack-visualizer: バンドルを視覚化および分析して、どのモジュールがスペースを占有しているか、および重複している可能性があるモジュールを確認します。
  • webpack-bundle-analyzer: バンドルコンテンツを便利なインタラクティブなズーム可能なツリーマップとして表現するプラグインおよびCLIユーティリティ。
  • webpack bundle optimize helper: このツールは、バンドルを分析し、バンドルサイズを削減するために改善すべき点に関する実用的な提案を提供します。
  • bundle-stats: バンドルレポート(バンドルサイズ、アセット、モジュール)を生成し、異なるビルド間の結果を比較します。
  • webpack-stats-viewer: webpackの統計情報用のビルドを含むプラグイン。webpackバンドルの詳細に関する詳細情報を表示します。

次のステップ

遅延ロードでは、実際のアプリケーションでimport()をどのように使用できるかについてより具体的な例を参照してください。また、キャッシュでは、コードをより効果的に分割する方法を学ぶことができます。

33 貢献者

pksjcepastelskysimon04jonwheelerjohnstewshinxitomtaschelevy9527rahulcschrisVillanuevarafdeshaunwallaceskipjackjakearchibaldTheDutchCoderrouzbeh84shaodahongsudarsangpkcoltonefreitasnEugeneHlushkoTiendo1011byzykAnayaDesignwizardofhogwartsmaximilianschmelzersmelukovchenxsanAdarahatesgoralsnitin315artem-malko