バンドル最適化

バンドル最適化とは、アプリケーションが一緒に使用されるコードを同じ場所に配置できるように、どのシンボルをどのバンドルに入れるかを決定するプロセスを指します。シンボルを同じ場所に配置することで、アプリケーションの読み込みを高速化できます。

シンボルとチャンク

シンボルは、Qwikにおける個々の遅延ロード可能な要素です。シンボルは、ソースコードで__$(__)を使用するたびに作成されます。

たとえば、以下のコードでは、component$onClick$から2つのシンボルが作成されます。

import { component$, useSignal } from '@builder.io/qwik';
 
export default component$(() => {
  const count = useSignal(0);
 
  return (
    <button onClick$={() => count.value++}>
      Increment {count.value}
    </button>
  );
});

オプティマイザーは上記のコードを次のように書き換えます。

ファイル: chunk-1.js

export default componentQRL(qrl('./chunk-1.js', 's_ABC123'));
 
export const s_ABC123 = () => {
  const count = useSignal(0);
 
  return (
    <button onClickQRL={qrl('/.chunk-1.js', 's_XYZ342')}>
      Increment {count.value}
    </button>
  );
};
 
export const s_XYZ432 = () => {
  const [count] = useLexicalScope();
  return count.value++;
}

上記の例では、すべてのシンボル(sABC123s_XYZ432)が同じチャンク(./chunk-1.js)に一緒に配置されています。

チャンクは、1つ以上のシンボルを含めることができるJavaScriptバンドルです。

最適なシンボル分散

バンドル最適化は、シンボルの配信を最適化できるスライダーとして考えることができます。

  • スライダーの一方の端には、すべてのシンボルを含む単一のチャンクがあります。これは、遅延ロードのないアプリケーションと同等です。(これが、今日ほとんどのアプリケーションが記述されている方法です。)
  • スライダーのもう一方の極端な端には、シンボルごとに別々のチャンクがあります。これが、開発中にQwikアプリケーションが動作する方法です。各シンボルは独自のチャンクに存在します。

単一のチャンクの問題点は次のとおりです。

  • クライアントが必要としない多くのシンボルが含まれます。(帯域幅の浪費。)
  • チャンク全体が読み込まれるまで、クライアントはシンボルを実行できません。

シンボルごとに別々のチャンクの問題点は次のとおりです。

  • クライアントは、すべてのチャンクを読み込むために多数のリクエストを行う必要があり、多くの場合、望ましくないウォーターフォールリクエストにつながります。

最適なソリューションは、中間地点にあります。チャンクの数を少なくしたいのですが、同時に、一緒に使用されるシンボルを同じチャンクに一緒に配置したいと考えています。チャンクの数を少なくすることで、チャンクを読み込む順序を優先できますが、同時にHTTPリクエストを行うコストを償却できます。シンボルを同じ場所に配置することで、ウォーターフォールを最小限に抑えることができます。

良い知らせは、Qwikを使用すると、どのシンボルがどのチャンクに入るかを完全に制御できるということです。通常、遅延ロードのためにアプリケーションを分割するには、開発者が動的インポートを記述し、コードをリファクタリングする必要があります。Qwikでは、すべての$()は潜在的な遅延ロード場所であり、必要なのは、バンドラーにシンボルをどのように分散するかを知らせることだけです。

qwikVite()プラグイン

vite.config.ts内のqwikVite()プラグインは、シンボルの分散を制御します。通常、entryStrategysmartに設定されており、Qwikがシンボルをどのように遅延ロードするかについての推測を行うことができます。ただし、vite.config.tsファイルで次のようにmanual構成を提供することで、ヒューリスティックをオーバーライドすることができます。

export default defineConfig(() => {
  const routesDir = resolve('src', 'routes');
  return {
    // ...
    qwikVite({
      entryStrategy: {
        type: 'smart',
        manual: {
          ...bundle('bundleA', [
              's_I5CyQjO9FjQ',
              's_NsnidK2eXPg',
              's_kDw0latGeM0',
          ]),
          ...bundle('bundleB', [
              's_vXb90XKAnjE',
              's_hYpp40gCb60',
          ]),
          ...bundle('bundleC', [
              's_AqHBIVNKf34',
              's_oEksvFPgMEM',
              's_eePwnt3YTI8',
          ]),
        },
      },
    }),
  };
});
 
function bundle(bundleName: string, symbols: string[]) {
  return symbols.reduce((obj, key) => {
    // Sometimes symbols are prefixed with `s_`, remove it.
    obj[key.replace('s_', '')] = obj[key] = bundleName;
    return obj;
  }, {} as Record<string, string>);
}

それでは、s_I5CyQjO9FjQのようなシンボル名をどのように取得するのでしょうか?次のセクション「ランタイム分析」を参照してください。

ランタイム分析

解決すべき根本的な問題は、最適なバンドルを静的に決定することは不可能であるということです。理想的なバンドルは、ユーザーの行動によって異なります。ユーザーの行動を観察した後でのみ、どのシンボルが一緒に使用されるかを判断できます。

実行中のアプリケーションからシンボルの使用状況を収集するには

  1. このコードをアプリに挿入します。
    <script>
      window.symbols = [];
      document.addEventListener('qsymbol', (e) => window.symbols.push(e.detail));
    </script>
  2. ユーザーの行動を模倣した一連の操作を実行します。
  3. コンソールを開き、「symbols」と入力して、使用されているシンボルのリストを表示します。その情報を使用して、vite.config.tsファイルを更新します。

注: 将来的には、この情報を収集するためのより良い方法を作成することを検討しています。(インサイトを参照してください。)

注: シンボルハッシュは、多くのコンパイルにまたがっても安定するように設計されています。ただし、コードが複雑なリファクタリングを経ると、ハッシュが変更される可能性があります。これにより、アプリケーションが壊れることはありませんが、ランタイム分析を再度収集できるまで、シンボルが最適ではない別のチャンクに移動する可能性があります。

サービスワーカー

Qwikアプリは、バンドルがブラウザのキャッシュにプリフェッチされることを保証し、ユーザーの操作がキャッシュヒットをもたらし、操作に遅延がないようにサービスワーカーを採用しています。

投機的なモジュールフェッチを参照してください。

サービスワーカー機能は、一部またはすべてのサポート対象ブラウザで、セキュアなコンテキスト(HTTPS)でのみ利用できることに注意してください。serviceWorkerプロパティAPI仕様を参照してください。

イベント

シンボルがいつロードされるかに関するすべての情報は、次のカスタムイベントをリッスンすることで観察できます。

qprefetch カスタムイベントの詳細。

qprefetch イベントは、新しいアプリケーションビューをレンダリングすることにより、新しいコードパスがユーザーに公開されたときに発生します。(たとえば、新しいモデルダイアログをレンダリングすると、新しいボタンが表示されます。ユーザーがボタンを操作したときに遅延が発生しないように、新しいボタンのコードをプリフェッチしたいと考えています。)通常、サービスワーカーは qprefetch イベントをリッスンし、シンボルをキャッシュにロードします。サービスワーカーは、シンボルとバンドルのマップを持っており、この情報を使用して、シンボルに基づいてどのバンドルをプリフェッチするかを決定します。

export interface QPrefetchDetail {
  /// A list of symbols to prefetch.
  symbols: string[];
}

qsymbol カスタムイベントの詳細。

qsymbol イベントは、Qwik がシンボルを解決する必要があるたびに発生します。このイベントをリッスンすると、アプリケーションによってさまざまなシンボルがいつロードされるかについての洞察を得ることができます。この情報は、必要なシンボルを同じバンドルにまとめることで、バンドルをより最適化するために使用できます。

export interface QSymbolDetail {
  /// Optional DOM event which triggered the symbol resolution.
  element?: Element;
  /// Request time when the symbol was resolved.
  reqTime: number;
  /// Symbol being resolved.
  symbol: string;
}

ウォーターフォール

サービスワーカーは、バンドルをプリフェッチすることにより、ウォーターフォールを最小限に抑えようとします。そのため、サービスワーカーはシンボルとチャンクの manifest を持っています。manifest は、すべてのシンボルとそれに対応するチャンクの完全なグラフを表します。また、インポートグラフも認識しているため、シンボルがプリフェッチされると、サービスワーカーはインポートグラフの一部として必要な他のすべてのシンボルもプリフェッチします。

コントリビューター

このドキュメントをより良くするために貢献してくれたすべてのコントリビューターに感謝します!

  • mhevery
  • the-r3aper7
  • mrhoodz
  • Craiqser
  • literalpie
  • antoinepairet
  • hamatoyogi