SPA を超えて - PWA の代替アーキテクチャ

アーキテクチャについて

ウェブアプリに使用するアーキテクチャと、プログレッシブ ウェブアプリの構築時にアーキテクチャ上の決定がどのように影響するかという重要なトピックを取り上げますが、誤解される可能性があります。

「アーキテクチャ」は曖昧に聞こえる場合があり、なぜこれが重要であるかはすぐにはわかりません。アーキテクチャを考える一つの方法は、ユーザーがサイトのページにアクセスしたときに、どの HTML が読み込まれるか、そして、ユーザーが別のページにアクセスしたときに何が読み込まれるかです。

これらの問いに対する答えは必ずしも単純なことではなく、プログレッシブ ウェブアプリについて考え始めると、その答えはさらに複雑になる可能性があります。私の目標は、私が効果的だと感じた可能なアーキテクチャを一つひとつ説明することです。この記事では、私が行った決定をプログレッシブ ウェブアプリの構築に対する「アプローチ」として説明します。

独自の PWA を作成する際は、私のアプローチを自由に使用できますが、それに代わる有効な方法は常にあります。すべての要素がどのように組み合わされているかを目の当たりにして、皆様のニーズに応えてカスタマイズしていただけたら幸いです。

Stack Overflow PWA

この記事に合わせて、Stack Overflow PWA を作成しました。私は多くの時間を Stack Overflow の閲覧と投稿に費やしており、特定のトピックに関するよくある質問を簡単に参照できるウェブアプリを構築したいと考えました。これは、一般公開されている Stack Exchange API 上に構築されています。オープンソースです。詳細は GitHub プロジェクトをご覧ください。

マルチページ アプリ(MPA)

詳細に入る前に用語を定義し 基盤となる技術について説明しましょうまずは「Multi Page App(MPA)」と名付けたい アプリについてお話しします

MPA は、ウェブの誕生以来使用されてきた従来のアーキテクチャの呼称です。ユーザーが新しい URL に移動するたびに、ブラウザはそのページに固有の HTML を順次レンダリングします。ナビゲーションの合間にページの状態やコンテンツを保持しようとしません。新しいページにアクセスするたびに、新しいページが開始されます。

これは、ウェブアプリを構築するシングルページ アプリ(SPA)モデルとは対照的です。SPA モデルでは、ユーザーが新しいセクションにアクセスしたときに、ブラウザが JavaScript コードを実行して既存のページを更新します。SPA と MPA はどちらも同様に有効なモデルですが、この投稿では、マルチページ アプリのコンテキストにおける PWA のコンセプトについて確認したいと思います。

高い信頼性

多くの人が「プログレッシブ ウェブアプリ」(PWA)という言い回しを耳にしたことがあるでしょう。背景資料の一部は、こちらのサイトの他のセクションですでにご存じかもしれません。

PWA は、最高水準のユーザー エクスペリエンスを提供し、ユーザーのホーム画面に実際に表示されるウェブアプリと考えることができます。「FIRE」は、Fast、Integrated、Reliable、Engaging の頭字語で、PWA の構築時に考慮すべきすべての属性がまとめられています。

この記事では、それらの属性のサブセットである [Fast] と [Reliable] について主に説明します。

高速: 「高速」の意味は状況によって異なりますが、ネットワークからの読み込みをできるだけ少なくすることによる速度上のメリットについて説明します。

信頼性: ただし、元の速度だけでは十分ではありません。ウェブアプリを PWA に見せかけるには、ウェブアプリが信頼できるものである必要があります。ネットワークの状態に関係なく、カスタマイズされたエラーページであっても、常に何かを読み込むために十分な復元力が必要です。

信頼性の高い高速性: 最後に、PWA の定義を少し言い換えて、信頼性の高い高速の構築の意味を確認します。低レイテンシのネットワークを使用している場合にのみ、高速で信頼性があるだけでは十分ではありません。信頼性が高いとは、基盤となるネットワーク状態に関係なく、ウェブアプリの速度が一定であることを意味します。

実現技術: Service Worker + Cache Storage API

PWA は、スピードとレジリエンスに高い要求事項をもたらします。幸いなことに、ウェブ プラットフォームには、そのようなパフォーマンスを実現するための構成要素がいくつか用意されています。これは Service WorkerCache Storage API です。

Cache Storage API を使用して、受信リクエストをリッスンし、一部をネットワークに渡して、後で使用できるようにレスポンスのコピーを保存する Service Worker を作成できます。

Cache Storage API を使用してネットワーク レスポンスのコピーを保存する Service Worker。

ウェブアプリが次に同じリクエストを行うと、Service Worker はキャッシュをチェックし、以前にキャッシュに保存されたレスポンスを返すだけです。

Cache Storage API を使用して応答し、ネットワークをバイパスする Service Worker。

可能な限りネットワークを避けることは、確実な高速パフォーマンスを提供するうえで非常に重要です。

「アイソモーフィック」JavaScript

また、「同型」または「ユニバーサル」な JavaScript とも呼ばれるコンセプトがもう 1 つあります。簡単に言うと、異なるランタイム環境間で同じ JavaScript コードを共有できます。PWA を作成する際に、バックエンド サーバーと Service Worker の間で JavaScript コードを共有したいと思っていました。

この方法でコードを共有する有効なアプローチは多数ありますが、私のアプローチは、最終的なソースコードとして ES モジュールを使用することでした。次に、BabelRollup を組み合わせて使用し、これらのモジュールをサーバーと Service Worker 用にトランスパイルしてバンドルしました。私のプロジェクトでは、ファイル拡張子が .mjs のファイルは ES モジュールに存在するコードです。

サーバー

これらのコンセプトと用語を念頭に置いて、実際に Stack Overflow PWA をどのように作成したかを詳しく見ていきましょう。まず バックエンドサーバーの概要から アーキテクチャ全体にどのように適合するかを説明します

動的バックエンドと静的ホスティングの組み合わせを探していましたが、Firebase プラットフォームを使用することにしました。

Firebase Cloud Functions は、受信リクエストがあるとノードベースの環境を自動的にスピンアップし、私がすでに使い慣れている一般的な Express HTTP フレームワークと統合します。また、サイトのすべての静的リソースにすぐに使えるホスティング機能も備えています。サーバーがリクエストを処理する方法を見ていきましょう

ブラウザがサーバーに対してナビゲーション リクエストを行うと、次のような流れになります。

サーバーサイドでのナビゲーション レスポンスの生成の概要。

サーバーは URL に基づいてリクエストを転送し、テンプレート ロジックを使用して完全な HTML ドキュメントを作成します。Stack Exchange API のデータと、サーバーがローカルに保存している部分的な HTML フラグメントを組み合わせて使用しています。Service Worker が応答方法を認識すると、ウェブアプリへの HTML のストリーミングを開始できます。

この図には、ルーティングとテンプレートという 2 つの部分があり、詳しく説明します。

ルーティング

ルーティングに関しては、Express フレームワークのネイティブ ルーティング構文を使用していました。単純な URL 接頭辞だけでなく、パスの一部としてパラメータを含む URL にも対応する十分な柔軟性があります。ここでは、照合する基本の Express パターンのルート名間のマッピングを作成します。

const routes = new Map([
  ['about', '/about'],
  ['questions', '/questions/:questionId'],
  ['index', '/'],
]);

export default routes;

これで、このマッピングをサーバーのコードから直接参照できます。特定の Express パターンに一致するものがあると、適切なハンドラが、そのマッチング ルートに固有のテンプレート ロジックで応答します。

import routes from './lib/routes.mjs';
app.get(routes.get('index'), async (req, res) => {
  // Templating logic.
});

サーバーサイド テンプレート

そのテンプレート ロジックはどのようなものでしょうか。部分的な HTML フラグメントを 1 つずつ順番に つなぎ合わせるアプローチを採用しましたこのモデルはストリーミングに適しています。

サーバーは最初の HTML ボイラープレートをすぐに送り返し、ブラウザはその部分的なページをすぐにレンダリングできます。サーバーは残りのデータソースをつなぎ合わせて、ドキュメントが完成するまでブラウザにストリーミングします。

Express コードでいずれかのルートを確認します。

app.get(routes.get('index'), async (req, res) => {
  res.write(headPartial + navbarPartial);
  const tag = req.query.tag || DEFAULT_TAG;
  const data = await requestData(...);
  res.write(templates.index(tag, data.items));
  res.write(footPartial);
  res.end();
});

response オブジェクトの write() メソッドを使用し、ローカルに保存されている部分テンプレートを参照することで、外部データソースをブロックすることなく、レスポンス ストリームをすぐに開始できます。ブラウザはこの最初の HTML を取得し、意味のあるインターフェースをレンダリングしてすぐにメッセージを読み込みます。

ページの次のセクションでは、Stack Exchange API のデータを使用します。このデータを取得するということは、サーバーがネットワーク リクエストを行う必要があるということです。ウェブアプリは、レスポンスを受け取って処理するまで何もレンダリングできませんが、少なくともユーザーが待機している間、空白の画面を見つめているわけではありません。

ウェブアプリは、Stack Exchange API からのレスポンスを受信すると、カスタム テンプレート関数を呼び出して、API から取得したデータを対応する HTML に変換します。

テンプレート言語

テンプレートは驚くほど議論を呼ぶトピックですので、私が考えたアプローチは数多くあります。特に、既存のテンプレート フレームワークと古い関係がある場合は、独自のソリューションに置き換える必要があります。

このユースケースでは、JavaScript のテンプレート リテラルを使用し、いくつかのロジックをヘルパー関数に分割することでした。MPA を作成する利点の一つは、状態の更新を追跡して HTML を再レンダリングする必要がないことです。そのため、静的 HTML を作成する基本的なアプローチがうまくいきました。

ここでは、ウェブアプリのインデックスの動的 HTML 部分をテンプレート化する方法の例を示します。自分のルートと同様に、テンプレート ロジックは ES モジュールに保存されており、サーバーと Service Worker の両方にインポートできます。

export function index(tag, items) {
  const title = `<h3>Top "${escape(tag)}" Questions</h3>`;
  const form = `<form method="GET">...</form>`;
  const questionCards = items
    .map(item =>
      questionCard({
        id: item.question_id,
        title: item.title,
      })
    )
    .join('');
  const questions = `<div id="questions">${questionCards}</div>`;
  return title + form + questions;
}

これらのテンプレート関数は純粋な JavaScript であり、必要に応じてロジックを小さなヘルパー関数に分割することをおすすめします。ここでは、API レスポンスで返された各アイテムを 1 つの関数に渡します。この関数は、すべての適切な属性を設定した標準の HTML 要素を作成します。

function questionCard({id, title}) {
  return `<a class="card"
             href="/questions/${id}"
             data-cache-url="${questionUrl(id)}">${title}</a>`;
}

特に注目すべき点は、各リンクに追加するデータ属性 data-cache-url です。これは、対応する質問を表示するために必要な Stack Exchange API の URL に設定します。覚えておいてください。後でまた訪れることにします。

ルートハンドラに戻り、テンプレートが完了したら、ページの HTML の最後の部分をブラウザにストリーミングして、ストリームを終了します。これは、プログレッシブ レンダリングが完了したことをブラウザに知らせる手がかりです。

app.get(routes.get('index'), async (req, res) => {
  res.write(headPartial + navbarPartial);
  const tag = req.query.tag || DEFAULT_TAG;
  const data = await requestData(...);
  res.write(templates.index(tag, data.items));
  res.write(footPartial);
  res.end();
});

以上がサーバー設定の概要ですウェブアプリに初めてアクセスするユーザーは常にサーバーからレスポンスを受け取りますが、訪問者がウェブアプリに戻ると、Service Worker が応答を開始します。それでは始めましょう

Service Worker

Service Worker でのナビゲーション レスポンスの生成の概要。

この図は見覚えがあるはずです。これまでに説明した要素の多くは、若干異なる配置になっています。ここでは、Service Worker を考慮に入れて、リクエスト フローについて説明します。

Service Worker は、指定された URL の受信ナビゲーション リクエストを処理し、私のサーバーと同様に、ルーティングとテンプレート ロジックを組み合わせて応答方法を判断します。

アプローチは前と同じですが、fetch()Cache Storage API などの低レベル プリミティブが異なります。これらのデータソースを使用して HTML レスポンスを作成し、Service Worker がウェブアプリに渡します。

Workbox

ここでは、低レベルのプリミティブを使用してゼロから始めるのではなく、Workbox という高レベルのライブラリのセット上に Service Worker を構築します。Service Worker のキャッシュ保存、ルーティング、レスポンス生成ロジックの強固な基盤となります。

ルーティング

サーバーサイドのコードと同様に、Service Worker は受信リクエストを適切なレスポンス ロジックと照合する方法を認識している必要があります。

私のアプローチは、regexparam という便利なライブラリを利用して、各 Express ルートを対応する正規表現変換することでした。変換が完了したら、Workbox の正規表現ルーティングの組み込みサポートを利用できます。

正規表現を含むモジュールをインポートした後、各正規表現を Workbox のルーターに登録します。各ルート内ではレスポンスを生成する カスタムテンプレートロジックを指定できますService Worker でのテンプレートはバックエンド サーバーよりも少し複雑ですが、Workbox は多くの手間のかかる作業に役立ちます。

import regExpRoutes from './regexp-routes.mjs';

workbox.routing.registerRoute(
  regExpRoutes.get('index')
  // Templating logic.
);

アセットの静的キャッシュ

テンプレート作成において重要なことは、部分的な HTML テンプレートが Cache Storage API を介してローカルで利用可能になり、ウェブアプリへの変更のデプロイ時に最新の状態に保たれるようにすることです。手動で行うとキャッシュのメンテナンスはエラーが発生しやすいため、ビルドプロセスの一環として Workbox を使用してプレキャッシュを処理します。

構成ファイルを使用して、事前キャッシュする URL を Workbox に指示します。このディレクトリには、すべてのローカル アセットと照合するパターンのセットが格納されているディレクトリを指定します。このファイルは、サイトを再構築するたびにrunされるワークボックスの CLI によって自動的に読み取られます。

module.exports = {
  globDirectory: 'build',
  globPatterns: ['**/*.{html,js,svg}'],
  // Other options...
};

Workbox は各ファイルの内容のスナップショットを作成し、その URL とリビジョンのリストを最終的な Service Worker ファイルに自動的に挿入します。Workbox は、事前キャッシュされたファイルを常に利用可能にし、最新の状態に保つために必要なものをすべて備えています。その結果、次のような内容を含む service-worker.js ファイルが作成されます。

workbox.precaching.precacheAndRoute([
  {
    url: 'partials/about.html',
    revision: '518747aad9d7e',
  },
  {
    url: 'partials/foot.html',
    revision: '69bf746a9ecc6',
  },
  // etc.
]);

より複雑なビルドプロセスを使用するユーザー向けに、Workbox にはコマンドライン インターフェースに加えて、webpack プラグイン汎用ノード モジュールが用意されています。

ストリーミング

次に、事前にキャッシュに保存された部分的な HTML を、Service Worker ですぐにウェブアプリにストリーミングして戻します。これは「確実に高速」にするための重要な部分です。私は常にすぐに何か意味のある何かを画面に表示します。これは、Service Worker 内で Streams API を使用することで実現できます。

Streams API については聞いたことがあるかもしれません。同僚のジェイク・アーチボルド(Jake Archibald)は 何年も前からこのアーティストを称賛しています彼は 2016 年がウェブ ストリームの年になると大胆に予測しました。Streams API は 2 年前と変わらない優れた 機能を備えていますが

当時は Chrome のみが Streams をサポートしていましたが、Streams API は現在ではさらに広くサポートされています。全体的に見て、適切なフォールバック コードがあれば、現在 Service Worker でのストリームの使用を妨げるものはありません。

Streams API の実際の動作について 頭を悩ませている点が 1 つあります非常に強力なプリミティブのセットが公開されており、使い慣れているデベロッパーは、次のような複雑なデータフローを作成できます。

const stream = new ReadableStream({
  pull(controller) {
    return sources[0]
      .then(r => r.read())
      .then(result => {
        if (result.done) {
          sources.shift();
          if (sources.length === 0) return controller.close();
          return this.pull(controller);
        } else {
          controller.enqueue(result.value);
        }
      });
  },
});

ただし、このコードの完全な意味を理解することは、すべての人に役立つとは限りません。このロジックを解析するのではなく、Service Worker のストリーミングに対するアプローチについて説明します。

新しい高レベルのラッパーである workbox-streams を使用しています。これにより、キャッシュとネットワークの両方からのランタイム データの両方から、複数のストリーミング ソースを組み合わせて渡すことができます。Workbox は、個々のソースの調整と、単一のストリーミング レスポンスへの結合を行います。

また、Workbox は Streams API がサポートされているかどうかを自動的に検出し、サポートされていない場合は同等のストリーミング以外のレスポンスを作成します。つまり、ストリームのブラウザ サポートが 100% に近づくため、フォールバックの書き込みを心配する必要はありません。

ランタイム キャッシュ

Service Worker が Stack Exchange API のランタイム データをどのように処理するかを見てみましょう。ウェブアプリのストレージが無限に増加しないように、Workbox に組み込まれた stale-while-revalidate キャッシュ戦略のサポートと有効期限を利用しています。

ストリーミング レスポンスを構成するさまざまなソースを処理するために、Workbox で 2 つの戦略を設定しました。Workbox を使用すると、いくつかの関数呼び出しと構成で、数百行の手書きコードを必要とする作業を実行できます。

const cacheStrategy = workbox.strategies.cacheFirst({
  cacheName: workbox.core.cacheNames.precache,
});

const apiStrategy = workbox.strategies.staleWhileRevalidate({
  cacheName: API_CACHE_NAME,
  plugins: [new workbox.expiration.Plugin({maxEntries: 50})],
});

1 つ目の方法では、部分的な HTML テンプレートのように、事前にキャッシュされたデータを読み取ります。

もう 1 つの戦略は、50 エントリに達したときの最も最近使用されたキャッシュの有効期限とともに、stale-while-revalidate キャッシュ ロジックを実装します。

戦略を導入したので、後はその戦略を使用して完全なストリーミング レスポンスを作成する方法を Workbox に伝えるだけです。ソースの配列を関数として渡すと、それぞれの関数がすぐに実行されます。Workbox は、各ソースから結果を取得してウェブアプリに順番にストリーミングし、配列内の次の関数がまだ完了していない場合にのみ遅延します。

workbox.streams.strategy([
  () => cacheStrategy.makeRequest({request: '/head.html'}),
  () => cacheStrategy.makeRequest({request: '/navbar.html'}),
  async ({event, url}) => {
    const tag = url.searchParams.get('tag') || DEFAULT_TAG;
    const listResponse = await apiStrategy.makeRequest(...);
    const data = await listResponse.json();
    return templates.index(tag, data.items);
  },
  () => cacheStrategy.makeRequest({request: '/foot.html'}),
]);

最初の 2 つのソースは、Cache Storage API から直接読み取られる事前キャッシュに保存された部分テンプレートであるため、常にすぐに使用できます。これにより、Service Worker の実装は、サーバー側のコードと同様に、リクエストへの応答を確実かつ迅速に行うことができます。

次のソース関数は、Stack Exchange API からデータを取得し、ウェブアプリが想定する HTML にレスポンスを処理します。

stale-while-revalidate 戦略では、この API 呼び出しに対して以前にキャッシュに保存されたレスポンスがある場合、そのレスポンスをすぐにページにストリーミングすると同時に、次回リクエストされたときに「バックグラウンド」のキャッシュ エントリを更新します。

最後に、フッターのキャッシュ コピーをストリーミングし、最終的な HTML タグを閉じてレスポンスを完成させます。

コードを共有すると、データの同期が維持されます

Service Worker のコードの一部に見覚えがあることがわかります。Service Worker で使用される部分的な HTML とテンプレートのロジックは、サーバーサイドのハンドラで使用されるものと同一です。このコード共有により、ウェブアプリに初めてアクセスする場合でも、Service Worker によってレンダリングされたページに戻る場合でも、一貫したエクスペリエンスをユーザーに提供できます。これが同型 JavaScript の優れた点です

動的で段階的な機能強化

ここまでは PWA のサーバーと Service Worker について説明しましたが、最後に説明するロジックは、ページが完全にストリーミングされた後、各ページで実行される少量の JavaScript です。

このコードによってユーザー エクスペリエンスが徐々に向上しますが、ウェブアプリは実行されていなくても動作しますが、これは重要ではありません。

ページ メタデータ

私のアプリは、クライアントサイドの JavaScipt を使用して、API レスポンスに基づいてページのメタデータを更新します。ページごとにキャッシュされた HTML の最初の部分と同じものを使用するため、ウェブアプリではドキュメントのヘッダーに汎用タグが使用されます。しかし、テンプレートとクライアント側コードを調整することで、ページ固有のメタデータを使用してウィンドウのタイトルを更新できます。

テンプレート コードの一部として、適切にエスケープされた文字列を含むスクリプトタグを含めることをおすすめします。

const metadataScript = `<script>
  self._title = '${escape(item.title)}';
</script>`;

ページが読み込まれた後、その文字列を読み取ってドキュメントのタイトルを更新します。

if (self._title) {
  document.title = unescape(self._title);
}

独自のウェブアプリで更新したいページ固有のメタデータが他にある場合は、同じ方法を使用できます。

オフライン UX

私が追加した他の段階的な機能強化は、Google のオフライン機能に注意を引くために使用されています。信頼性の高い PWA を作成しましたが、オフラインのときでも以前アクセスしたページを読み込めることをユーザーに知らせてほしいです。

まず、Cache Storage API を使用して、以前にキャッシュに保存された API リクエストのリストを取得し、URL のリストに変換します。

前に説明した特別なデータ属性には、質問を表示するために必要な API リクエストの URL が含まれていることを覚えていますか?これらのデータ属性をキャッシュに保存された URL のリストと照らし合わせて、一致しないすべての質問リンクの配列を作成できます。

ブラウザがオフライン状態になると、キャッシュされていないリンクのリストをループ処理し、機能しないリンクはグレー表示します。これは、ユーザーがそれらのページに何を期待できるかに関する視覚的なヒントにすぎないことに留意してください。実際にリンクを無効にしたり、ユーザーが移動できないようにしたりしているわけではありません。

const apiCache = await caches.open(API_CACHE_NAME);
const cachedRequests = await apiCache.keys();
const cachedUrls = cachedRequests.map(request => request.url);

const cards = document.querySelectorAll('.card');
const uncachedCards = [...cards].filter(card => {
  return !cachedUrls.includes(card.dataset.cacheUrl);
});

const offlineHandler = () => {
  for (const uncachedCard of uncachedCards) {
    uncachedCard.style.opacity = '0.3';
  }
};

const onlineHandler = () => {
  for (const uncachedCard of uncachedCards) {
    uncachedCard.style.opacity = '1.0';
  }
};

window.addEventListener('online', onlineHandler);
window.addEventListener('offline', offlineHandler);

主な注意点

ここまでは、マルチページ PWA を作成する手法を見てきました。独自のアプローチを考える際は多くの要素を考慮する必要があり、結果的に私がこれまでと異なる選択をすることになるかもしれません。その柔軟性は、ウェブ向けの開発における優れた点の 1 つです。

アーキテクチャに関する決定をする際に陥りやすい一般的な注意点がいくつかあります。ここでは、その負担を軽減したいと思います。

完全な HTML をキャッシュに保存しない

HTML ドキュメント全体をキャッシュに保存することはおすすめしません。1 つは、スペースの無駄だからです。ウェブアプリのページごとに同じ基本 HTML 構造を使用している場合、同じマークアップのコピーが何度も保存されることになります。

さらに重要な点として、サイトの共有 HTML 構造に変更をデプロイすると、以前にキャッシュに保存されたページはすべて古いレイアウトで残ったままになります。リピーターに古いページと新しいページが混在する状況を想像してみてください。

サーバー / Service Worker のドリフト

もう一つの注意点として、サーバーと Service Worker が同期しなくなるという問題があります。私のアプローチは、同じコードが両方の場所で実行されるように、同型 JavaScript を使用することでした。既存のサーバー アーキテクチャによっては、これが常に可能とは限りません。

アーキテクチャに関する決定がどのようなものであっても、サーバーと Service Worker で同等のルーティングとテンプレートのコードを実行するための戦略が必要です。

最悪のケースのシナリオ

レイアウト / デザインに一貫性がない

このような落とし穴を無視するとどうなるでしょうか。あらゆる種類のエラーが発生する可能性がありますが、最悪のケースでは、キャッシュされたページを再ユーザーが非常に古いレイアウトで訪問することがあります。たとえば、ヘッダー テキストが古い、有効ではなくなった CSS クラス名が使用されている場合などです。

最悪のシナリオ: ルーティングの破損

あるいは、Service Worker では処理されず、サーバーでは処理されている URL に遭遇する場合もあります。ゾンビ レイアウトやデッドエンドが多いサイトは、信頼できない PWA です。

成功のためのヒント

しかし、あなたは一人ではありません。このような問題を回避するには、次のヒントを参考にしてください。

多言語実装のテンプレートおよびルーティング ライブラリを使用する

JavaScript 実装を含むテンプレート ライブラリとルーティング ライブラリを使用します。すべてのデベロッパーが現在のウェブサーバーやテンプレート言語から 移行する余裕があるわけではありません

ただし、一般的なテンプレート化とルーティングのフレームワークの多くは、複数の言語で実装されています。現在のサーバーの言語だけでなく JavaScript でも動作するものが見つかれば、Service Worker とサーバーの同期の維持に一歩近づきます。

ネストされたテンプレートよりも順次テンプレートを優先する

次に、順次ストリーミングできる一連のテンプレートを使用することをおすすめします。ページの後の部分でより複雑なテンプレート ロジックを使用しても問題ありません。ただし、HTML の最初の部分をできるだけ速くストリーミングできれば問題ありません。

Service Worker で静的コンテンツと動的コンテンツの両方をキャッシュに保存する

最適なパフォーマンスを得るには、サイトの重要な静的リソースをすべて事前にキャッシュに保存する必要があります。API リクエストなどの動的コンテンツを処理するために、ランタイム キャッシュ ロジックを設定する必要もあります。Workbox を使用すると、すべてをゼロから実装するのではなく、十分にテストされた本番環境に対応した戦略に基づいて構築できます。

どうしても必要な場合にのみネットワークでブロックする

これに関連して、キャッシュからレスポンスをストリーミングできない場合にのみ、ネットワーク上でブロックするようにします。多くの場合、キャッシュされた API レスポンスをすぐに表示すると、新しいデータを待機するよりもユーザー エクスペリエンスが向上します。

リソース