日が経過しています。情報の鮮度にご注意ください
はじめに
webpackが非同期ロードを実現したい場合、つまりメインモジュールを先にロードし、特定のモジュールまたは複数のモジュール(つまりバンドル後のchunk)が必要な場合にリクエストを送信してロードすることを目指しています。
この目的はもちろんページの初回ロード速度を向上させることですが、避けられない追加のリクエストが発生します。これらはトレードオフの関係にあり、ここでは非同期ロードの詳細について説明します。
本文
実装は主にrequire.ensure([], callback)というものに依存しています。正直に言うと、私がこれに気づいたのはwebpack.config.jsのoutputフィールドにchunkFilenameというフィールドがあったからです。この「根掘り葉掘り」の癖がある私は、これとfilenameフィールドの違いを知りたくなり、調べてみるとfilename(仮にbundle.jsとする)はページに必要なすべてのjsをバンドルし、最終的に生成される総合的なjs(もちろんマルチページの場合は共通モジュールを抽出できますが、これは今回の焦点ではありません)。一方、chunkFilenameは非エントリーポイント(entryに列挙されているフィールド)のchunkファイルをバンドルして生成されたファイルで、主にオンデマンドでの非同期モジュールロード時に使用されます。
これらのファイルはbundle.jsにバンドルされておらず、一部(すべてではない)のモジュールに依存しているだけで、同時に非同期ロードが必要なため、require.ensureを使用して追加のjsにバンドルされます。これらのjsは、最終的にbundle.jsによってscriptタグが作成され、appendによってページにロードされます:
1 | |
OK、これらはすべて理解しやすいですが、公式ドキュメントを確認する際に、いくつか注意すべき詳細点を発見しました。
CommonJS と AMD require時の違い
CommonJSはrequire.ensure([''], callback)を使用して非同期ロードモジュールを処理します。AMDは一般的なAMDモジュールと同様に、requireの配列依存形式で処理されますrequire([''], callback)。
しかし、CommonJSが配列内のモジュールをロードする際は、ロードのみで実行はされません。callback内で再度requireされた場合にのみ実行されます:
1 | |
require.ensureメソッドは、コールバック呼び出し時にdependencies内のすべての依存関係が同期的にrequireできることを保証します。require関数の実装がコールバックのパラメータとして送信されます。
また、このcallbackのパラメータは、requireインターフェースを実装した関数(おそらく単なるrequire関数の参照)です。
このchunkFilenameはoutput内のchunkFilename設定で上書きされます。
一方、AMDは通常の依存関係前置きであるため、require時点でモジュールが実行されます:
1 | |
OK、AMDの例は馴染みがないので、以下ではCommonJSを例にいくつかの詳細を説明します。
まず、require.ensureにcallbackを渡すと、コールバック関数内でrequireされるモジュールもすべて最終的な非同期ロードファイルにバンドルされます。
chunk バンドル最適化戦略
- 2つの
chunkが同じモジュールを含む場合、それらは1つにマージされます。 - あるモジュールが
chunkのすべての親chunkで利用可能な場合、そのモジュールはchunkから削除されます。 - ある
chunkが別のchunkのすべてのモジュールを含む場合、最終的により多くのmoduleを含むchunkがバンドルされます。このルールは、1つのchunkが他の複数のchunkのすべてのmoduleを含む場合にも適用されます。
2番目は理解しにくいかもしれませんが、実際には、エントリーファイルA.jsにbモジュールが含まれており、require.ensureを使用して生成されたchunk.jsファイルにもこのbモジュールが含まれている場合、require.ensureはA.jsファイルで呼び出されるため、A.jsはこのchunk.jsの親chunkと見なされ、最終的に生成されるchunk.jsに含まれるbモジュールの内容は削除されます。在所有父级 chunk 都可用は1番目で説明した状況を指します:いくつかのchunkが同じmoduleを含む場合、最終的に生成されるbundle.jsは1つだけですが、このchunkが複数の親chunk(つまりentryに対応するchunkファイル)を持つ可能性があります。
検証してみましょう:
エントリーファイルapp.jsのコード:
1 | |
別のエントリーファイルapp2.jsのコード:
1 | |
ensure.jsのコード:
1 | |
ensure2.jsのコード:
1 | |
最後に、子chunkと親chunkの両方が存在するif_be_remove.jsのコード:
1 | |
ChromeブラウザのコンソールでNetworkにロードされたjsの内容を確認してみましょう(ここでは[id].[name].jsの命名方式を使用しています)
app.jsページ:

app2.jsページ:

分かるように、if_be_remove.jsは2つのchunk、つまり1-love.jsと3.hate.jsで参照されており、同時にこれら2つのchunkの親、すなわちapp.jsとapp2.jsでも参照されているため、これら2つのchunkではif_be_remove.jsのコードは出現していません。
補足: chunkの概念と定義
ここで補足しますが、いわゆるchunkとは、1つまたは複数のmoduleで構成される独立したjsファイルを指し、chunkは以下のタイプに分類されます:
Entry Chunks:Entry Chunksは最も一般的なChunksタイプで、私たち自身が書いたビジネスロジック関連のコード(ほとんどの場合、固有のコード、つまり共通chunksに抽出されないコード)を含み、通常はInitial Chunksのロードが完了してから実行されます(またはmodule番号0のモジュールに遭遇した場合)。Normal Chunks:Normal Chunksは主にアプリケーション実行時に動的にロードされるモジュールを指し、WebpackはJSONPのような適切なローダーを作成して動的ロードを行います。Initial Chunks:Initial Chunksは本質的にはNormal Chunksですが、アプリケーション初期化時にロードが完了します。このタイプのChunksはCommonsChunkPluginによって生成されることが多く、グローバルなモジュール位置情報を含んでいます。Entry chunksのコード実行はこのchunkに依存するため、このjsを優先的にロードする必要があります。
先の例では、共通jsとしてパッケージ化され、すべてまたは一部のページで使用されるbundule.jsはInitial Chunksであり、現在のページでのみ使用される chunk(app.xxxxxx.jsなど)はEntry Chunks、require.ensureを通じて非同期ロードされるchunk(3-hate``1-loveなど)はNormal Chunksです。
人生の重要な選択に直面したとき、最善の方法を誰かが教えてくれて、貴重な時間を無駄にせずに済めばと、私はよく願っています。だからこそ、自分の経験を踏まえて頻繁にブログを書き、広大なインターネットのこの小さな片隅に、私にとって一度きりの人生経験を記録し、助けを求める人々の力になれればと思っています。