コード分割 JavaScript

大量の JavaScript リソースを読み込むと、ページの読み込み速度に大きく影響します。JavaScript を小さなチャンクに分割し、起動時にページが機能するために必要なもののみをダウンロードすることで、ページの読み込み応答性を大幅に改善し、ページの Interaction to Next Paint(INP)を改善できます。

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

これは多くの場合、JavaScript がメインスレッドで解析およびコンパイルされるときにメインスレッドがブロックされるために発生します。この処理に時間がかかりすぎると、インタラクティブなページ要素がユーザー入力にすぐに反応しないことがあります。この問題を回避する方法の一つは、ページが機能するために必要な JavaScript だけを読み取って、コード分割と呼ばれる手法を使用して、後で他の JavaScript の読み込みを遅らせることです。このモジュールでは、この 2 つの手法のうち後者に焦点を当てます。

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

Lighthouse では、JavaScript の実行に 2 秒以上かかると警告がスローされ、3.5 秒を超えると失敗します。JavaScript の過剰な解析と実行は、ページのライフサイクルのいずれかの時点で潜在的な問題になります。ユーザーがページで操作した時刻と、JavaScript の処理と実行を担うメインスレッド タスクの実行時に一致すると、インタラクションの入力遅延が長くなる可能性があります。

さらに、JavaScript の過剰な実行や解析は、最初のページ読み込み時に特に問題になります。これは、ページのライフサイクルの中で、ユーザーがページを操作する可能性が非常に高いポイントであるためです。実際、読み込み応答性の指標である Total Blocking Time(TBT)INP強い相関関係があることから、ユーザーが最初のページ読み込み時にインタラクションを試行する傾向が高いことを示唆しています。

Lighthouse の監査では、ページがリクエストする各 JavaScript ファイルの実行に要した時間が報告されます。これにより、どのスクリプトがコード分割の対象になるかを正確に特定できるようになります。さらに、Chrome DevTools のカバレッジ ツールを使用して、ページ読み込み時にページの JavaScript のどの部分が使用されていないかを正確に特定できます。

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

  • JavaScript はページの読み込み時に必要なため、他の時点では読み込めません。
  • 後で読み込み可能な残りの JavaScript。ほとんどの場合、ユーザーがページ上の特定のインタラクティブ要素を操作するタイミングで読み込みます。

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

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

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

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

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

動的インポートのデモ

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 のみが含まれます(構成されている場合は、リソース名にファイル ハッシュが含まれます)。このチャンクは、condition が truthy の場合にのみダウンロードされます。

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

一方、SplitChunksPlugin 構成を変更して chunks: initial を指定すると、コードは最初のチャンクでのみ分割されます。このようなチャンクは、静的にインポートされたチャンクや、webpack の entry プロパティにリストされているチャンクです。上記の例では、結果として得られるチャンクは、1 つのスクリプト ファイル内の form-validation.js main.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 だけではありません。イメージ要素と <iframe> 要素は、それ自体がコストが高くなる可能性があるリソースです。JavaScript と同様に、画像と <iframe> 要素の読み込みを遅らせるには、遅延読み込みを行います。これについては、このコースの次のモジュールで説明します。