Webpack の非同期オンデマンドローディング

✍🏼 作成日 2016年05月02日   
❗️ 注意:この記事が作成されてから既に 日が経過しています。情報の鮮度にご注意ください

はじめに

webpackが非同期ロードを実現したい場合、つまりメインモジュールを先にロードし、特定のモジュールまたは複数のモジュール(つまりバンドル後のchunk)が必要な場合にリクエストを送信してロードすることを目指しています。

この目的はもちろんページの初回ロード速度を向上させることですが、避けられない追加のリクエストが発生します。これらはトレードオフの関係にあり、ここでは非同期ロードの詳細について説明します。

本文

実装は主にrequire.ensure([], callback)というものに依存しています。正直に言うと、私がこれに気づいたのはwebpack.config.jsoutputフィールドにchunkFilenameというフィールドがあったからです。この「根掘り葉掘り」の癖がある私は、これとfilenameフィールドの違いを知りたくなり、調べてみるとfilename(仮にbundle.jsとする)はページに必要なすべてのjsをバンドルし、最終的に生成される総合的なjs(もちろんマルチページの場合は共通モジュールを抽出できますが、これは今回の焦点ではありません)。一方、chunkFilenameは非エントリーポイント(entryに列挙されているフィールド)のchunkファイルをバンドルして生成されたファイルで、主にオンデマンドでの非同期モジュールロード時に使用されます。

これらのファイルはbundle.jsにバンドルされておらず、一部(すべてではない)のモジュールに依存しているだけで、同時に非同期ロードが必要なため、require.ensureを使用して追加のjsにバンドルされます。これらのjsは、最終的にbundle.jsによってscriptタグが作成され、appendによってページにロードされます:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// This file contains only the entry chunk.
// The chunk loading function for additional chunks
__webpack_require__.e = function requireEnsure(chunkId, callback) {
// "0" is the signal for "already loaded"
if (installedChunks[chunkId] === 0)
return callback.call(null, __webpack_require__);

// an array means "currently loading".
if (installedChunks[chunkId] !== undefined) {
installedChunks[chunkId].push(callback);
} else {
// start chunk loading
installedChunks[chunkId] = [callback];
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.charset = 'utf-8';
script.async = true;

script.src = __webpack_require__.p + '' + ({}[chunkId] || chunkId) + '.js';
head.appendChild(script);
}
};

OK、これらはすべて理解しやすいですが、公式ドキュメントを確認する際に、いくつか注意すべき詳細点を発見しました。

CommonJSAMD require時の違い

CommonJSrequire.ensure([''], callback)を使用して非同期ロードモジュールを処理します。AMDは一般的なAMDモジュールと同様に、requireの配列依存形式で処理されますrequire([''], callback)

しかし、CommonJSが配列内のモジュールをロードする際は、ロードのみで実行はされません。callback内で再度requireされた場合にのみ実行されます:

1
2
3
4
5
6
7
require(['./other/ensure.js', './other/ensure2.js'], function () {
var ensure = require('./other/ensure.js');
var ensure2 = require('./other/ensure2.js');

module1();
module2();
}, chunkFilename);

require.ensureメソッドは、コールバック呼び出し時にdependencies内のすべての依存関係が同期的にrequireできることを保証します。require関数の実装がコールバックのパラメータとして送信されます。

また、このcallbackのパラメータは、requireインターフェースを実装した関数(おそらく単なるrequire関数の参照)です。

このchunkFilenameoutput内のchunkFilename設定で上書きされます。

一方、AMDは通常の依存関係前置きであるため、require時点でモジュールが実行されます:

1
2
3
4
5
6
7
require(['./other/ensure.js', './other/ensure2.js'], function (
ensure,
ensure2
) {
ensure();
ensure2();
});

OK、AMDの例は馴染みがないので、以下ではCommonJSを例にいくつかの詳細を説明します。

まず、require.ensurecallbackを渡すと、コールバック関数内でrequireされるモジュールもすべて最終的な非同期ロードファイルにバンドルされます。

chunk バンドル最適化戦略

  1. 2つのchunkが同じモジュールを含む場合、それらは1つにマージされます。
  2. あるモジュールがchunkのすべての親chunkで利用可能な場合、そのモジュールはchunkから削除されます。
  3. あるchunkが別のchunkのすべてのモジュールを含む場合、最終的により多くのmoduleを含むchunkがバンドルされます。このルールは、1つのchunkが他の複数のchunkのすべてのmoduleを含む場合にも適用されます。

2番目は理解しにくいかもしれませんが、実際には、エントリーファイルA.jsbモジュールが含まれており、require.ensureを使用して生成されたchunk.jsファイルにもこのbモジュールが含まれている場合、require.ensureA.jsファイルで呼び出されるため、A.jsはこのchunk.jsの親chunkと見なされ、最終的に生成されるchunk.jsに含まれるbモジュールの内容は削除されます。在所有父级 chunk 都可用は1番目で説明した状況を指します:いくつかのchunkが同じmoduleを含む場合、最終的に生成されるbundle.jsは1つだけですが、このchunkが複数の親chunk(つまりentryに対応するchunkファイル)を持つ可能性があります。

検証してみましょう:

エントリーファイルapp.jsのコード:

1
2
3
4
5
6
7
8
require('../other/if_be_remove.js')();
require.ensure(
['../other/ensure.js'],
function () {
require('../other/ensure.js')();
},
'love'
);

別のエントリーファイルapp2.jsのコード:

1
2
3
4
5
6
7
8
require('../other/if_be_remove.js')();
require.ensure(
['../other/ensure2.js'],
function () {
require('../other/ensure2.js')();
},
'hate'
);

ensure.jsのコード:

1
2
3
4
require('./if_be_remove.js')();
module.exports = function () {
console.log("i'm be ensure!");
};

ensure2.jsのコード:

1
2
3
4
require('./if_be_remove.js')();
module.exports = function () {
console.log("i'm be ensure2!");
};

最後に、子chunkと親chunkの両方が存在するif_be_remove.jsのコード:

1
2
3
module.exports = function () {
console.log('im be removed!');
};

ChromeブラウザのコンソールでNetworkにロードされたjsの内容を確認してみましょう(ここでは[id].[name].jsの命名方式を使用しています)

app.jsページ:

webpack-async

app2.jsページ:

webpack-async

分かるように、if_be_remove.jsは2つのchunk、つまり1-love.js3.hate.jsで参照されており、同時にこれら2つのchunkの親、すなわちapp.jsapp2.jsでも参照されているため、これら2つのchunkではif_be_remove.jsのコードは出現していません。

補足: chunkの概念と定義

ここで補足しますが、いわゆるchunkとは、1つまたは複数のmoduleで構成される独立したjsファイルを指し、chunkは以下のタイプに分類されます:

  1. Entry Chunks: Entry Chunksは最も一般的なChunksタイプで、私たち自身が書いたビジネスロジック関連のコード(ほとんどの場合、固有のコード、つまり共通chunksに抽出されないコード)を含み、通常はInitial Chunksのロードが完了してから実行されます(またはmodule番号0のモジュールに遭遇した場合)。
  2. Normal Chunks: Normal Chunksは主にアプリケーション実行時に動的にロードされるモジュールを指し、WebpackJSONPのような適切なローダーを作成して動的ロードを行います。
  3. Initial Chunks: Initial Chunksは本質的にはNormal Chunksですが、アプリケーション初期化時にロードが完了します。このタイプのChunksCommonsChunkPluginによって生成されることが多く、グローバルなモジュール位置情報を含んでいます。Entry chunksのコード実行はこのchunkに依存するため、このjsを優先的にロードする必要があります。

先の例では、共通jsとしてパッケージ化され、すべてまたは一部のページで使用されるbundule.jsInitial Chunksであり、現在のページでのみ使用される chunkapp.xxxxxx.jsなど)はEntry Chunksrequire.ensureを通じて非同期ロードされるchunk3-hate``1-loveなど)はNormal Chunksです。

- EOF -
この記事の初出: Webpack の非同期オンデマンドローディング - Xheldon Blog