コード分割 JavaScript

サイズの大きい JavaScript リソースを読み込むと、ページの読み込み速度に大きな影響があります。JavaScript をより小さなチャンクに分割し、ページの起動に必要なものだけをダウンロードすることで、ページの読み込みの応答性を大幅に改善できます。これにより、ページの次のペイントまでのインタラクション(INP)も改善されます。

サイズの大きい JavaScript ファイルをダウンロード、解析、コンパイルしている間、ページが応答しなくなることがあります。ページ要素は、ページの初期 HTML の一部であり、CSS でスタイル設定されているため、表示されます。ただし、これらのインタラクティブ要素を動作させるために必要な JavaScript や、ページによって読み込まれる他のスクリプトが、それらの要素を動作させるために JavaScript を解析して実行している可能性があります。その結果、ユーザーはインタラクションが大幅に遅延した、あるいは完全に機能しなくなったと感じる可能性があります。

これは通常、JavaScript がメインスレッドで解析およびコンパイルされるため、メインスレッドがブロックされることが原因で発生します。この処理に時間がかかりすぎると、インタラクティブなページ要素がユーザー入力に十分な速さで応答しない可能性があります。この問題を解決する 1 つの方法は、ページが機能するために必要な JavaScript のみを読み込み、他の JavaScript はコード分割と呼ばれる手法で後で読み込むように遅延させることです。このモジュールでは、後者の手法に焦点を当てます。

コード分割により、起動時の JavaScript の解析と実行を減らす

Lighthouse は、JavaScript の実行に 2 秒以上かかると警告をスローし、3.5 秒以上かかると失敗します。JavaScript の解析と実行が過剰になると、ページのライフサイクルのどの時点でも問題が発生する可能性があります。ユーザーがページを操作するタイミングと、JavaScript の処理と実行を担当するメインスレッドのタスクが実行されるタイミングが重なると、インタラクションの入力遅延が増加する可能性があります。

さらに、過剰な JavaScript の実行と解析は、特にページのライフサイクルのうちユーザーがページを操作する可能性が高い初期ページ読み込み時に問題となります。実際、読み込みの応答性指標である合計ブロック時間(TBT)INP との相関性が高いことがわかっています。これは、ユーザーが最初のページ読み込み中に操作を試みる傾向が強いことを示しています。

ページがリクエストする各 JavaScript ファイルの実行時間をレポートする Lighthouse の監査は、どのスクリプトがコード分割の候補になるかを正確に特定するのに役立ちます。さらに、Chrome DevTools のカバレッジ ツールを使用して、ページの読み込み中に使用されない JavaScript の部分を正確に特定することもできます。

コード分割は、ページの初期 JavaScript ペイロードを削減できる便利な手法です。JavaScript バンドルを次の 2 つの部分に分割できます。

  • ページの読み込み時に必要な JavaScript であり、他のタイミングで読み込むことはできません。
  • 残りの JavaScript は、後で読み込むことができます。多くの場合、ユーザーがページ上の特定のインタラクティブ要素を操作したときに読み込まれます。

コード分割は、動的 import() 構文を使用して行うことができます。この構文は、起動時に特定の JavaScript リソースをリクエストする <script> 要素とは異なり、ページ ライフサイクルの後半で JavaScript リソースをリクエストします。

document.querySelectorAll('#myForm input').addEventListener('blur', async () => {
  // Get the form validation named export from the module through destructuring:
  const { validateForm } = await import('/validate-form.mjs');

  // Validate the form:
  validateForm();
}, { once: true });

上記の JavaScript スニペットでは、ユーザーがフォームの <input> フィールドのいずれかをぼかした場合にのみ、validate-form.mjs モジュールがダウンロード、解析、実行されます。この場合、フォームの検証ロジックを駆動する JavaScript リソースは、実際に使用される可能性が最も高い場合にのみページに関与します。

webpackParcelRollupesbuild などの JavaScript バンドラは、ソースコードで動的な import() 呼び出しを検出するたびに、JavaScript バンドルをより小さなチャンクに分割するように構成できます。これらのツールのほとんどはこれを自動的に行いますが、特に esbuild ではこの最適化を有効にする必要があります。

コード分割に関する役立つメモ

コード分割は、ページの初回読み込み時のメインスレッドの競合を減らす効果的な方法ですが、コード分割の機会を探すために JavaScript ソースコードの監査を行う場合は、いくつかの点に注意する必要があります。

可能であればバンドラを使用する

開発プロセスでJavaScript モジュールを使用することは、デベロッパーにとって一般的なプラクティスです。コードの可読性と保守性を向上させる優れた開発者エクスペリエンスの改善です。ただし、JavaScript モジュールを本番環境に配信すると、パフォーマンスが最適でない特性が生じる可能性があります。

最も重要なのは、コード分割を予定しているモジュールを含め、ソースコードを処理して最適化するためにバンドラーを使用することです。バンドラーは、JavaScript ソースコードに最適化を適用するだけでなく、バンドルサイズと圧縮率などのパフォーマンスに関する考慮事項のバランスを取るうえでも非常に効果的です。圧縮の有効性はバンドルサイズとともに増加しますが、バンドラーは、スクリプト評価による長いタスクが発生するほどバンドルが大きくならないようにします。

また、バンドラーは、ネットワーク経由で大量のバンドルされていないモジュールを送信する問題も回避します。JavaScript モジュールを使用するアーキテクチャは、大規模で複雑なモジュール ツリーを持つ傾向があります。モジュールツリーをバンドルしない場合、各モジュールは個別の HTTP リクエストを表します。モジュールをバンドルしないと、ウェブアプリのインタラクティビティが遅延する可能性があります。<link rel="modulepreload"> リソースヒントを使用して、できるだけ早く大きなモジュールツリーを読み込むことは可能ですが、読み込みパフォーマンスの観点からは、JavaScript バンドルの方が望ましいです。

ストリーミング コンパイルを誤って無効にしない

Chromium の V8 JavaScript エンジンには、本番環境の JavaScript コードを可能な限り効率的に読み込むための最適化が多数用意されています。このような最適化の 1 つに、ストリーミング コンパイルがあります。これは、ブラウザにストリーミングされる HTML の増分解析と同様に、ネットワークから到着した JavaScript のストリーミング チャンクをコンパイルします。

Chromium でウェブ アプリケーションのストリーミング コンパイルを確実に行うには、いくつかの方法があります。

  • JavaScript モジュールを使用しないように本番環境コードを変換します。バンドラーは、コンパイル ターゲットに基づいて JavaScript ソースコードを変換できます。ターゲットは多くの場合、特定の環境に固有のものです。V8 は、モジュールを使用しない JavaScript コードにストリーミング コンパイルを適用します。また、JavaScript モジュール コードを JavaScript モジュールとその機能を使用しない構文に変換するようにバンドラーを構成することもできます。
  • JavaScript モジュールを本番環境にリリースする場合は、.mjs 拡張機能を使用します。本番環境の JavaScript でモジュールを使用するかどうかに関係なく、モジュールを使用する JavaScript と使用しない JavaScript に特別なコンテンツ タイプはありません。V8 に関しては、.js 拡張機能を使用して本番環境で JavaScript モジュールを配信する場合、ストリーミング コンパイルを事実上オプトアウトすることになります。JavaScript モジュールに .mjs 拡張機能を使用すると、V8 はモジュールベースの JavaScript コードのストリーミング コンパイルが中断されないようにすることができます。

これらの考慮事項を理由にコード分割の使用を思いとどまる必要はありません。コード分割は、ユーザーへの初期 JavaScript ペイロードを削減する効果的な方法ですが、バンドラーを使用して、V8 のストリーミング コンパイル動作を維持する方法を知ることで、本番環境の JavaScript コードをユーザーにとって可能な限り高速にすることができます。

動的インポートのデモ

webpack

webpack には SplitChunksPlugin という名前のプラグインが付属しています。このプラグインを使用すると、バンドラーが JavaScript ファイルを分割する方法を構成できます。webpack は、動的 import() ステートメントと静的 import ステートメントの両方を認識します。SplitChunksPlugin の動作は、構成で chunks オプションを指定することで変更できます。

  • chunks: async はデフォルト値で、動的な import() 呼び出しを指します。
  • chunks: initial は、静的 import 呼び出しを指します。
  • chunks: all は動的インポート import() と静的インポートの両方をカバーし、async インポートと initial インポートの間でチャンクを共有できます。

デフォルトでは、webpack が動的 import() ステートメントを検出するたびに、そのモジュール用に個別のチャンクが作成されます。

/* main.js */

// An application-specific chunk required during the initial page load:
import myFunction from './my-function.js';

myFunction('Hello world!');

// If a specific condition is met, a separate chunk is downloaded on demand,
// rather than being bundled with the initial chunk:
if (condition) {
  // Assumes top-level await is available. More info:
  // https://v8.dev/features/top-level-await
  await import('/form-validation.js');
}

上記のコード スニペットのデフォルトの webpack 構成では、2 つの別々のチャンクが生成されます。

  • main.js./my-function.js モジュールを含む main.js チャンク(webpack が initial チャンクとして分類するチャンク)。
  • async チャンク(form-validation.js のみを含む。構成されている場合は、リソース名にファイル ハッシュを含む)。このチャンクは、conditiontruthy の場合にのみダウンロードされます。

この構成により、実際に必要になるまで form-validation.js チャンクの読み込みを遅らせることができます。これにより、最初のページ読み込み時のスクリプト評価時間を短縮することで、読み込みの応答性を向上させることができます。form-validation.js チャンクのスクリプトのダウンロードと評価は、指定された条件が満たされたときに発生します。この場合、動的にインポートされたモジュールがダウンロードされます。たとえば、特定のブラウザでのみポリフィルがダウンロードされる条件や、前の例のように、ユーザー操作にインポートされたモジュールが必要な条件などがあります。

一方、SplitChunksPlugin 構成を変更して chunks: initial を指定すると、コードは初期チャンクでのみ分割されます。これらは、静的にインポートされたチャンクや、webpack の entry プロパティにリストされているチャンクなどです。上記の例では、結果として得られるチャンクは 1 つのスクリプト ファイル内の form-validation.jsmain.js の組み合わせになり、初期ページ読み込みのパフォーマンスが低下する可能性があります。

SplitChunksPlugin のオプションを構成して、大きなスクリプトを複数の小さなスクリプトに分割することもできます。たとえば、maxSize オプションを使用して、maxSize で指定されたサイズを超えるチャンクを個別のファイルに分割するように webpack に指示します。大きなスクリプト ファイルを小さなファイルに分割すると、読み込みの応答性が向上する可能性があります。CPU 使用率の高いスクリプト評価作業が小さなタスクに分割され、メインスレッドが長時間ブロックされる可能性が低くなるためです。

また、JavaScript ファイルのサイズが大きくなると、スクリプトでキャッシュの無効化が発生しやすくなります。たとえば、フレームワークとファースト パーティ アプリケーション コードの両方を含む非常に大きなスクリプトを配信する場合、フレームワークのみが更新され、バンドルされたリソースの他の部分が更新されないと、バンドル全体が無効になる可能性があります。

一方、スクリプト ファイルを小さくすると、再訪問者がキャッシュからリソースを取得する可能性が高まり、再訪問時のページ読み込みが速くなります。ただし、小さいファイルは大きいファイルほど圧縮のメリットが少なく、ブラウザのキャッシュがプライミングされていない状態でページを読み込むと、ネットワークのラウンド トリップ時間が長くなる可能性があります。キャッシュ保存の効率、圧縮の有効性、スクリプトの評価時間のバランスを取る必要があります。

webpack デモ

webpack SplitChunksPlugin デモ

理解度テスト

コード分割を実行するときに使用される import ステートメントのタイプはどれですか?

動的 import()
正解です。
静的 import
もう一度お試しください。

JavaScript モジュールの先頭に記述する必要があり、他の場所には記述できない import ステートメントはどれですか?

動的 import()
もう一度お試しください。
静的 import
正解です。

webpack で SplitChunksPlugin を使用する場合、async チャンクと initial チャンクの違いは何ですか?

async チャンクは動的 import() を使用して読み込まれ、initial チャンクは静的 import を使用して読み込まれます。
正解です。
async チャンクは静的 import を使用して読み込まれ、initial チャンクは動的 import() を使用して読み込まれます。
もう一度お試しください。

次のステップ: 画像と <iframe> 要素の遅延読み込み

JavaScript は比較的コストの高いリソースタイプですが、読み込みを遅延できるのは JavaScript だけではありません。画像要素と <iframe> 要素は、それ自体がコストのかかるリソースになる可能性があります。JavaScript と同様に、遅延読み込みによって画像と <iframe> 要素の読み込みを遅らせることができます。これについては、このコースの次のモジュールで説明します。