投機的モジュールフェッチング

Qwikは、JavaScriptを使わずに起動できるため、ページを非常に高速にロードしてインタラクティブにすることができます。これに加えて、投機的モジュールフェッチングは、Qwikがバックグラウンドスレッドでブラウザのキャッシュを事前に入力できるようにする強力な機能です。

Qwikの目標は、ユーザーの潜在的なインタラクションに基づいて、アプリケーションの必要な部分のみをキャッシュすることでロードを最適化することです。どのインタラクションが可能でないかを理解することで、不要なバンドルのロードを回避します。

キャッシュの事前入力

各ページロードは、その時点でユーザーがページ上で実行できるバンドルでキャッシュを事前に入力します。たとえば、ページにボタンのクリックリスナーがあるとします。ページがロードされると、サービスワーカーの最初のジョブは、そのクリックリスナーのコードがすでにキャッシュ内にあることを確認することです。ユーザーがボタンをクリックすると、Qwikはイベントリスナーの関数と、その関数を実行するためのコード依存関係にリクエストを行います。目標は、コードがすでに実行準備完了のブラウザのキャッシュ内にあることです。

最初のページロードは、次の可能性のあるインタラクションのためにキャッシュを準備し、別のスレッドで他の必要なコードを段階的にダウンロードします。モーダルやメニューを開くなど、フォローアップインタラクションが発生すると、Qwikは最後のインタラクション以降に使用できる可能性のある追加コードで別のイベントを発行します。ユーザーがアプリケーションを操作すると、キャッシュの事前入力は継続的に行われます。

キャッシュの事前入力イベント

推奨される戦略は、サービスワーカーを使用してブラウザのキャッシュを事前入力することです。Qwikフレームワーク自体は、すでにデフォルトであるprefetchEvent実装を使用する必要があります。

サービスワーカーを使用したキャッシュの事前入力

従来、サービスワーカーはアプリケーションが使用するほとんどまたはすべてのバンドルをキャッシュするために使用されます。サービスワーカーは、アプリケーションをオフラインで動作させる方法としてのみ見られるのが一般的です。

Qwik Cityは、サービスワーカーをまったく異なる方法で使用して、強力なキャッシング戦略を提供します。アプリケーション全体をダウンロードする代わりに、サービスワーカーを使用して、実行可能なものでキャッシュを動的に事前入力することを目標とします。アプリケーション全体をダウンロードしないことで、リソースが解放され、ユーザーは画面上で現在のタスクを完了するために使用できる必要な部分のみをリクエストできるようになります。

さらに、サービスワーカーは、Qwikから発行されたこれらのイベントのリスナーを自動的に追加します。

バックグラウンドタスク

サービスワーカーを使用する利点は、ワーカーの拡張機能でもあり、バックグラウンドスレッドで実行されることです。

Web Workerを使用すると、Webアプリケーションのメイン実行スレッドとは別のバックグラウンドスレッドでスクリプト操作を実行できます。この利点は、労力を要する処理を別のスレッドで実行できるため、メイン(通常はUI)スレッドがブロック/減速することなく実行できることです。

サービスワーカー(ワーカーの一種)内でキャッシュを事前に設定することで、メインUIスレッドを妨げないように、バックグラウンドタスクでコードを実行することができます。メインスレッドを妨げないことで、ユーザー向けのQwikアプリケーションのパフォーマンスを向上させることができます。

インタラクティブにキャッシュを事前設定する

Qwik自体は、prefetchEvent実装を使用するように構成する必要があります。これはデフォルトです。Qwikがイベントを発行すると、サービスワーカーの登録は、インストールされてアクティブなサービスワーカーにイベントデータを積極的に転送します。

バックグラウンドスレッドで実行されているサービスワーカーは、モジュールをフェッチし、ブラウザのキャッシュに追加します。メインスレッドは、必要なバンドルに関するデータを発行するだけでよく、サービスワーカーはこれらのバンドルをキャッシュすることに専念します。

  1. ブラウザがすでにキャッシュしている場合?素晴らしい、何もしないでください!
  2. ブラウザがこのバンドルをまだキャッシュしていない場合は、フェッチリクエストを開始しましょう。

サービスワーカーは、同じバンドルに対する複数のリクエストが同時に発生しないようにします

リクエストとレスポンスのペアのキャッシュ

多くの従来のフレームワークでは、rel属性がprefetchpreload、またはmodulepreloadに設定されたHTML <link>タグを使用するのが推奨される戦略です。ただし、既知の問題により、Qwikはデフォルトのプリフェッチ戦略としてこのアプローチの使用を回避していますが、必要に応じて構成することもできます。

代わりに、QwikはブラウザのCache APIを最大限に活用する新しいアプローチを使用することを推奨しています。これは、modulepreloadよりもサポートが優れています。

Cache API

Cache APIは、多くの場合サービスワーカーと関連付けられており、アプリケーションがオフラインで動作できるように、リクエストとレスポンスのペアを保存する方法です。アプリケーションが接続なしで動作できるようにすることに加えて、同じCache APIは、Qwikで利用可能な非常に強力なキャッシュメカニズムを提供します。

インストールされアクティブ化されたサービスワーカーを使用してリクエストをインターセプトすることで、Qwikは既知のバンドルに対する特定のリクエストを処理できます。サービスワーカーの一般的な使用方法とは対照的に、デフォルトではすべてのリクエストを処理しようとはせず、Qwikによって生成された既知のバンドルのみを処理します。サイトにインストールされたサービスワーカーは、各サイトでカスタマイズできます。

Qwikのオプティマイザーの利点は、q-manifest.jsonファイルを生成することです。q-manifest.jsonには、バンドルがどのように関連付けられているか、および各バンドル内にどのシンボルが含まれているかの詳細なモジュールグラフが含まれています。この同じモジュールグラフデータがサービスワーカーに提供されるため、既知のバンドルに対するすべてのネットワークリクエストをキャッシュで処理できます。

動的インポートとキャッシュ

Qwikがモジュールをリクエストするときは、動的なimport()を使用します。たとえば、ユーザーインタラクションが発生し、Qwikが/build/q-abc.jsの動的インポートを実行する必要があるとします。そのためのコードは次のようになります。

const module = await import('/build/q-abc.js');

ここで重要なのは、Qwik自体がプリフェッチ戦略またはキャッシュ戦略を認識していないことです。単にURLをリクエストしているだけです。ただし、サービスワーカーをインストールし、サービスワーカーがリクエストをインターセプトしているため、URLを検査し、「これは/build/q-abc.jsに対するリクエストだ!これは私たちのバンドルの1つだ!実際のネットワークリクエストを行う前に、これがすでにキャッシュにあるかどうかをまず確認しよう」と言うことができます。

これがサービスワーカーとCache APIの力です!別のスレッドで、Qwikはユーザーが間もなくリクエストする可能性のあるモジュールのキャッシュを事前設定します。これらのモジュールがすでにキャッシュされている場合、ブラウザは何もしなくても済みます。

ネットワークリクエストの並列化

リクエストとレスポンスのペアのキャッシュに関するドキュメントでは、CacheService Worker APIの強力な組み合わせについて説明しました。ただし、Qwikでは、バックグラウンドスレッド内から、同じバンドルに対して重複するリクエストが作成されないようにし、ネットワークのウォーターフォールを防ぐことでさらに進化させることができます。

重複リクエストの回避

例として、エンドユーザーの接続が非常に遅いとします。最初にランディングページをリクエストすると、デバイスはHTMLをダウンロードしてコンテンツをレンダリングします(これはQwikが本当に得意な領域です)。この低速な接続では、ユーザーがアプリを動作させてインタラクティブにするために、さらに数百キロバイトをダウンロードする必要があるのは残念なことです。

ただし、アプリがQwikで構築されているため、エンドユーザーはアプリ全体をダウンロードしてインタラクティブにする必要はありません。代わりに、エンドユーザーはすでにSSRレンダリングされたHTMLアプリをダウンロードしており、「カートに追加」ボタンなどのインタラクティブな部分はすぐにプリフェッチできます。

実際には、コンポーネントツリーのレンダリング関数のスタック全体ではなく、実際のリスナーコードのみをプリフェッチしていることに注意してください。

低速な接続のデバイスという、この非常に一般的な現実の例では、デバイスはエンドユーザーに表示される可能性のあるインタラクションのキャッシュをすぐに事前設定し始めます。ただし、低速な接続のため、バックグラウンドスレッドで可能な限り早くモジュールをキャッシュし始めたにもかかわらず、フェッチリクエスト自体がまだ実行中である可能性があります。

デモの目的で、このバンドルのフェッチに2秒かかるとします。ただし、ページを1秒表示した後、ユーザーがボタンをクリックします。

従来のフレームワークでは、何も起こらない可能性が高いです!フレームワークがダウンロード、ハイドレーション、および再レンダリングを完了していない場合、イベントリスナーをボタンに追加することはできません。つまり、ユーザーのインタラクションは失われる可能性があります。

Qwikのキャッシング戦略を使用すると、ユーザーがボタンをクリックし、1秒前にリクエストを開始していて、完全に受信されるまで残り1秒しかない場合、エンドユーザーはその1秒を待つだけで済みます。このデモでは、低速な接続を使用していることを思い出してください。幸運なことに、ユーザーはすでに完全にレンダリングされたランディングページを受け取っており、完成したページをすでに見ています。次に、インタラクションできるアプリの部分でのみキャッシュを事前設定し、その低速な接続はそれらのバンドル(複数可)専用になります。これは、1つのリスナーを実行するためだけに、低速な接続でアプリ全体をダウンロードすることとは対照的です。

Qwikは既知のバンドルのリクエストをインターセプトできます。バックグラウンドスレッドでフェッチが実行中で、ユーザーが同じバンドルをリクエストした場合、2番目のリクエストが同じバンドルを再利用できるようにします。これは、ダウンロードが完了している可能性があります。リンクでこれを実行しようとすると、QwikがデフォルトでキャッシュAPIを使用し、サービスワーカーでリクエストをインターセプトすることを推奨した理由と、リンクを使用する代わりにその理由もわかります。

ネットワークウォーターフォールの削減

ネットワークウォーターフォールは、複数のリクエストが1つずつ順番に実行されるときに発生します。この順番の処理は、必要なすべてのモジュールをダウンロードする時間が、すべてのモジュールが同時に並行してダウンロードを開始するシナリオと比較して長くなるため、パフォーマンスを大幅に低下させる可能性があります。

以下は、3つのモジュール(A、B、C)の例です。モジュールAはBをインポートし、BはCをインポートします。HTMLドキュメントは、最初にモジュールAをリクエストすることでウォーターフォールを開始するものです。

import './b.js';
console.log('Module A');
import './c.js';
console.log('Module B');
console.log('Module C');
<script type="module" src="./a.js"></script>

この例では、最初にモジュールAがリクエストされたとき、ブラウザはモジュールBCもリクエストを開始する必要があることを認識していません。モジュールAのダウンロードが完了するまで、モジュールBのリクエストを開始する必要があることすらわかりません。ブラウザは、各モジュールのダウンロードが完了するまで、事前にリクエストを開始する必要があるものを認識していないという一般的な問題です。

ただし、サービスワーカーにはマニフェストから生成されたモジュールグラフが含まれているため、次にリクエストされるすべてのモジュールを認識しています。そのため、ユーザーインタラクションまたはバンドルのプリフェッチが発生すると、ブラウザはリクエストされるすべてのバンドルのリクエストを開始します。これにより、すべてのバンドルのリクエストにかかる時間を大幅に短縮できます。

ユーザーサービスワーカーコード

Qwik Cityによってインストールされたデフォルトのサービスワーカーは、アプリケーションによって完全に制御できます。たとえば、ソースファイルsrc/routes/service-worker.tsは、ブラウザによってリクエストされるスクリプトである/service-worker.jsになります。src/routes内のその場所は、ディレクトリベースのルーティングパターンに従っていることに注意してください。

以下は、デフォルトのsrc/routes/service-worker.tsソースファイルの例です

import { setupServiceWorker } from '@builder.io/qwik-city/service-worker';
 
setupServiceWorker();
 
addEventListener('install', () => self.skipWaiting());
 
addEventListener('activate', (ev) => ev.waitUntil(self.clients.claim()));

src/routes/service-worker.tsのソースコードは、Qwik Cityのサービスワーカーの設定へのオプトインまたはオプトアウトを含め、変更できます。

setupServiceWorker()関数が@builder.io/qwik-city/service-workerからインポートされ、ソースファイルの先頭で実行されることに注意してください。開発者は、必要に応じてこの関数がいつどこで呼び出されるかを柔軟に変更できます。たとえば、開発者が最初にフェッチリクエストを処理したい場合は、setupServiceWorker()の上にフェッチリスナーを追加できます。または、Qwik Cityのサービスワーカーをまったく使用したくない場合は、ファイルからsetupServiceWorker()を削除するだけです。

さらに、デフォルトのsrc/routes/service-worker.tsファイルには、installおよびactivateイベントリスナーが付属しており、それぞれファイルの末尾に追加されています。提供されるコールバックは、推奨されるコールバックです。ただし、開発者は自分のアプリの要件に応じてこれらのコールバックを変更できます。

もう一つ重要な注意点として、Qwik City のリクエスト傍受は、Qwik のバンドルのみを対象としており、ビルドの一部ではないリクエストを処理しようとはしません。

したがって、Qwik City はバンドルのプリフェッチとキャッシュを支援する方法を提供しますが、アプリのサービスワーカーを完全に制御するわけではありません。これにより、開発者は Qwik と競合することなく、独自のサービスワーカーロジックを追加できます。

開発中の無効化

投機的モジュールフェッチは、プレビューまたは本番ビルドでのみ有効になります。開発中はサービスワーカーが無効になり、投機的モジュールフェッチも無効になります。これは、開発中は、以前にキャッシュされたものではなく、常に最新の開発コードが使用されるようにするためです。

HTTPキャッシュとサービスワーカーキャッシュ

投機的モジュールフェッチが動作していないように見えるのは、キャッシュのさまざまなレベルが原因の一部である可能性があります。たとえば、ブラウザ自体がリクエストをHTTPキャッシュにキャッシュしたり、サービスワーカーがリクエストをCache APIにキャッシュしたりする場合があります。一方のキャッシュを空にするだけでは、投機的モジュールフェッチの効果を確認するには十分ではない可能性があります。

誤解を招くキャッシュの削除とハードリロード

開発者がキャッシュの削除とハードリロードを実行すると、実際にはブラウザのHTTPキャッシュをのみ空にしているため、少し誤解を招きます。ただし、サービスワーカーのキャッシュは空にしていません。ブラウザのHTTPキャッシュが空でも、サービスワーカーには以前にキャッシュされたリクエストが残っています。

さらに、「キャッシュの削除とハードリロード」を使用すると、ブラウザはサーバーへのリクエストno-cacheキャッシュ制御ヘッダーを送信します。リクエストにno-cacheキャッシュ制御ヘッダーがあるため、サービスワーカーは意図的に独自のキャッシュを使用せず、代わりにブラウザが通常どおりHTTPフェッチを再度実行します。

サービスワーカーキャッシュの空にする

投機的モジュールフェッチをテストするための推奨される方法は次のとおりです。

  • サービスワーカーの登録解除: Chrome DevTools で、[アプリケーション] タブに移動し、[サービスワーカー] の下にある、サイトのサービスワーカーの [登録解除] リンクをクリックします。
  • 「QwikBuild」キャッシュストレージの削除: Chrome DevTools で、[アプリケーション] タブに移動し、左側の [キャッシュストレージ] の下にある、「QwikBuild」キャッシュストレージを右クリックして [削除] を選択します。
  • ハードリロードをしない: ハードリロードすると、サービスワーカーにno-cacheキャッシュ制御が送信されるため、ハードリロードする代わりに、URLバーをクリックしてEnterキーを押します。これにより、初めて訪問者であるかのように通常のリクエストが送信されます。

このプロセスは、投機的モジュールフェッチをテストするためのみのものであり、新しいビルドには必要ないことに注意してください。各ビルドは新しいサービスワーカーを作成し、古いサービスワーカーは自動的に登録解除されます。

デバッグモード

Qwikコアのサービスワーカーは、root.tsx<PrefetchServiceWorker />および<PrefetchGraph />コンポーネントを使用しており、デバッグモードを備えています。

サービスワーカーのログを表示するには、JavaScriptコンソールにwindow.qwikPrefetchSW.push(['verbose', '', []])を追加し、Enterキーを押します。

貢献者

このドキュメントの改善にご協力いただいたすべての貢献者に感謝します。

  • ulic75
  • mhevery
  • adamdbradley
  • hamatoyogi
  • manucorporat
  • mrhoodz
  • thejackshelton
  • zanettin
  • wtlin1228
  • aendel
  • jemsco