Service Worker のキャッシュ戦略

これまで、Cache インターフェースについて言及し、小さなコード スニペットしか使用できませんでした。Service Worker を効果的に使用するには、1 つ以上のキャッシュ戦略を採用する必要があります。これには、Cache インターフェースについて多少の知識が必要です。

キャッシュ戦略は、Service Worker の fetch イベントと Cache インターフェースとのインタラクションです。キャッシュ戦略の記述方法は異なります。たとえば、静的アセットに対するリクエストをドキュメントとは異なる方法で処理することが望ましい場合があります。これはキャッシュ戦略の構成方法に影響します。

戦略そのものに取り掛かる前に、Cache インターフェースがないこと、それがどのようなものか、Service Worker のキャッシュを管理するためのいくつかのメソッドについて簡単に説明します。

Cache インターフェースと HTTP キャッシュ

Cache インターフェースを使用したことがない場合は、HTTP キャッシュと同じ、または少なくとも HTTP キャッシュに関連するものだと考えたくなるかもしれません。これは明細書や請求書ではありません。

  • Cache インターフェースは、HTTP キャッシュとはまったく異なるキャッシュ メカニズムです。
  • HTTP キャッシュに影響を与えるために使用する Cache-Control 構成は、Cache インターフェースに格納されるアセットには影響しません。

ブラウザ キャッシュは階層構造になっていると考えてください。HTTP キャッシュは、HTTP ヘッダーで表現されたディレクティブを含む Key-Value ペアによって駆動される低レベルキャッシュです。

一方、Cache インターフェースは、JavaScript API によって駆動される高レベルのキャッシュです。これは、比較的単純な HTTP Key-Value ペアを使用する場合よりも柔軟性が高く、キャッシュ戦略を可能にする方法の半分です。Service Worker のキャッシュに関する重要な API メソッドは次のとおりです。

  • CacheStorage.open: 新しい Cache インスタンスを作成します。
  • ネットワーク レスポンスを Service Worker キャッシュに保存する Cache.addCache.put
  • Cache.match: Cache インスタンス内でキャッシュに保存されたレスポンスを見つけます。
  • Cache.delete: キャッシュに保存されたレスポンスを Cache インスタンスから削除します。

これらはほんの一例です。他にも便利なメソッドがありますが、これらは、このガイドの後半で使用する基本的なメソッドです。

シンプルな fetch イベント

キャッシュ戦略のもう半分は、Service Worker の fetch イベントです。このドキュメントで「ネットワーク リクエストのインターセプト」について少し触れましたが、Service Worker 内の fetch イベントでこの処理が行われます。

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('install', (event) => {
  event.waitUntil(caches.open(cacheName));
});

self.addEventListener('fetch', async (event) => {
  // Is this a request for an image?
  if (event.request.destination === 'image') {
    // Open the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Respond with the image from the cache or from the network
      return cache.match(event.request).then((cachedResponse) => {
        return cachedResponse || fetch(event.request.url).then((fetchedResponse) => {
          // Add the network response to the cache for future visits.
          // Note: we need to make a copy of the response to save it in
          // the cache and use the original as the request response.
          cache.put(event.request, fetchedResponse.clone());

          // Return the network response
          return fetchedResponse;
        });
      });
    }));
  } else {
    return;
  }
});

これは簡単な例であり、実際の動作もご覧になれますが、Service Worker でできることを垣間見ることができます。上記のコードでは、次の処理を行います。

  1. リクエストの destination プロパティを調べて、これが画像リクエストかどうかを確認します。
  2. 画像が Service Worker のキャッシュにある場合は、そこから提供します。そうでない場合は、ネットワークから画像を取得し、レスポンスをキャッシュに保存して、ネットワーク レスポンスを返します。
  3. その他のリクエストはすべて、キャッシュを操作することなく Service Worker を通過します。

フェッチの event オブジェクトには request プロパティが含まれています。このプロパティには、各リクエストのタイプの特定に役立ついくつかの有用な情報が含まれています。

  • url: 現在 fetch イベントによって処理されているネットワーク リクエストの URL。
  • method: リクエスト メソッド(例:GETPOST など)。
  • mode: リクエストのモードを記述します。多くの場合、'navigate' の値は、HTML ドキュメントのリクエストを他のリクエストと区別するために使用されます。
  • destination。リクエストされたアセットのファイル拡張子を使用しない方法でリクエストされるコンテンツのタイプを記述します。

繰り返しになりますが、非同期はゲームの名前です。 install イベントには event.waitUntil メソッドが用意されています。このメソッドは、Promise が解決されるのを待ってから、有効化を続行します。fetch イベントでは同様の event.respondWith メソッドが用意されており、これを使用して非同期 fetch リクエストの結果や、Cache インターフェースの match メソッドから返されるレスポンスを返すことができます。

キャッシュ戦略

Cache インスタンスと fetch イベント ハンドラについて十分に理解したところで、Service Worker のキャッシュ戦略について詳しく見ていきましょう。実質的には可能性は無限大ですが、このガイドでは Workbox に付属する戦略をそのまま使用して、Workbox の内部がどのようになっているかを理解できます。

キャッシュのみ

ページから Service Worker、キャッシュへのフローを示しています。

まず、「キャッシュのみ」と呼ぶ簡単なキャッシュ戦略を見てみましょう。Service Worker がページを制御している場合、一致したリクエストはキャッシュに送信されます。つまり、パターンを機能させるには、キャッシュに保存されたアセットを事前キャッシュしておく必要があり、Service Worker が更新されるまでキャッシュ内のアセットが更新されることはありません。

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

// Assets to precache
const precachedAssets = [
  '/possum1.jpg',
  '/possum2.jpg',
  '/possum3.jpg',
  '/possum4.jpg'
];

self.addEventListener('install', (event) => {
  // Precache assets on install
  event.waitUntil(caches.open(cacheName).then((cache) => {
    return cache.addAll(precachedAssets);
  }));
});

self.addEventListener('fetch', (event) => {
  // Is this one of our precached assets?
  const url = new URL(event.request.url);
  const isPrecachedRequest = precachedAssets.includes(url.pathname);

  if (isPrecachedRequest) {
    // Grab the precached asset from the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      return cache.match(event.request.url);
    }));
  } else {
    // Go to the network
    return;
  }
});

上の例では、アセットの配列がインストール時に事前キャッシュされます。 Service Worker がフェッチを処理する際、fetch イベントによって処理されたリクエスト URL が、事前キャッシュされたアセットの配列内にあるかどうかが確認されます。ある場合は、キャッシュからリソースを取得し、ネットワークをスキップします。他のリクエストはネットワークのみを通過します。この戦略の実際の動作を確認するには、コンソールを開いてこちらのデモをご覧ください

ネットワークのみ

ページから Service Worker、ネットワークへのフローを示しています。

「Cache Only」の反対は「Network Only」です。この場合、Service Worker のキャッシュとやり取りすることなく、Service Worker を介してリクエストがネットワークに渡されます。これは、コンテンツの鮮度(マークアップなど)を確保するために適した方法ですが、その反面、ユーザーがオフラインのときは機能しません。

リクエストがネットワークを通過するようにすることは、一致するリクエストで event.respondWith を呼び出さないことを意味します。明示的に指定する場合は、ネットワークに渡すリクエストの fetch イベント コールバックで空の return; をスラップします。これは、プリキャッシュされていないリクエストの「キャッシュのみ」戦略のデモで発生します。

最初にキャッシュし、ネットワークにフォールバック

ページから Service Worker、キャッシュ、さらにキャッシュにない場合はネットワークというフローを示します。

この戦略では、物事がより複雑になります。 一致リクエストの場合、プロセスは次のようになります。

  1. リクエストがキャッシュにヒットした。アセットがキャッシュにあれば、そこから配信します。
  2. リクエストがキャッシュにない場合は、ネットワークに移動します。
  3. ネットワーク リクエストが完了したら、リクエストをキャッシュに追加し、ネットワークからレスポンスを返します。

この戦略の例を以下に示します。これはライブデモで試すことができます。

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  // Check if this is a request for an image
  if (event.request.destination === 'image') {
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Go to the cache first
      return cache.match(event.request.url).then((cachedResponse) => {
        // Return a cached response if we have one
        if (cachedResponse) {
          return cachedResponse;
        }

        // Otherwise, hit the network
        return fetch(event.request).then((fetchedResponse) => {
          // Add the network response to the cache for later visits
          cache.put(event.request, fetchedResponse.clone());

          // Return the network response
          return fetchedResponse;
        });
      });
    }));
  } else {
    return;
  }
});

この例では画像のみを示していますが、この方法はすべての静的アセット(CSS、JavaScript、画像、フォントなど)、特にハッシュ バージョニングに適用できる優れた方法です。HTTP キャッシュが開始するサーバーに対して、コンテンツの鮮度チェックをサイドステップ実行することで、不変のアセットの処理速度を向上させることができます。さらに重要な点は、キャッシュに保存されたアセットはオフラインで使用可能です。

ネットワーク ファースト、キャッシュにフォールバック

ページから Service Worker、ネットワークへ、さらにネットワークが利用できない場合にキャッシュへと続くフローを示します。

このように「最初にキャッシュ、次にネットワーク」を反転すると、「まずネットワーク、次にキャッシュ」という戦略になります。

  1. まずネットワークにアクセスしてリクエストを行い、そのレスポンスをキャッシュに保存します。
  2. 後でオフラインになった場合は、キャッシュ内のそのレスポンスの最新バージョンにフォールバックします。

この戦略は、オンライン中にリソースの最新バージョンが必要で、利用可能な最新バージョンへのオフライン アクセスを許可する場合の HTML または API リクエストに適しています。 HTML のリクエストに適用されると、次のようになります。

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  // Check if this is a navigation request
  if (event.request.mode === 'navigate') {
    // Open the cache
    event.respondWith(caches.open(cacheName).then((cache) => {
      // Go to the network first
      return fetch(event.request.url).then((fetchedResponse) => {
        cache.put(event.request, fetchedResponse.clone());

        return fetchedResponse;
      }).catch(() => {
        // If the network is unavailable, get
        return cache.match(event.request.url);
      });
    }));
  } else {
    return;
  }
});

これはデモで試すことができます。まず、そのページに移動します。HTML レスポンスをキャッシュに保存する前に、再読み込みが必要になる場合があります。次に、デベロッパー ツールでオフライン接続をシミュレートして、もう一度再読み込みします。 利用可能な最後のバージョンが即座にキャッシュから提供されます。

オフライン機能が重要であるものの、その機能と、ビットのマークアップや API データの最新バージョンへのアクセスのバランスを取る必要がある場合、「まずはネットワーク、次にキャッシュ」は、この目標を達成するための確実な戦略です。

未更新の再検証

ページから Service Worker、キャッシュ、さらにネットワークからキャッシュへのフローを示しています。

これまでに説明した戦略の中で最も複雑なのが「Stale-while-revalidate」です。最後の 2 つの戦略といくつかの点で類似していますが、この手順では、リソースのアクセス速度を優先しながら、バックグラウンドでリソースを常に最新の状態に維持します。この戦略は次のようなものです。

  1. アセットに対する最初のリクエストで、アセットをネットワークから取得してキャッシュに保存し、ネットワーク レスポンスを返します。
  2. 後続のリクエストでは、まずキャッシュからアセットを配信し、次に「バックグラウンドで」ネットワークに対して再度リクエストして、アセットのキャッシュ エントリを更新します。
  3. それ以降のリクエストについては、前のステップでキャッシュに格納したネットワークから取得された最後のバージョンが返されます。

これは、最新情報を把握する必要があるものの重要ではない情報に適した戦略です。ソーシャル メディア サイトのアバターなどが、新しいバージョンはユーザーが頻繁に更新されると更新されますが、必ずしもすべてのリクエストで最新バージョンが必要になるわけではありません。

// Establish a cache name
const cacheName = 'MyFancyCacheName_v1';

self.addEventListener('fetch', (event) => {
  if (event.request.destination === 'image') {
    event.respondWith(caches.open(cacheName).then((cache) => {
      return cache.match(event.request).then((cachedResponse) => {
        const fetchedResponse = fetch(event.request).then((networkResponse) => {
          cache.put(event.request, networkResponse.clone());

          return networkResponse;
        });

        return cachedResponse || fetchedResponse;
      });
    }));
  } else {
    return;
  }
});

この実際の動作は、さらに別のライブデモで確認できます。特に、ブラウザのデベロッパー ツールのネットワーク タブと、ブラウザの CacheStorage ビューア(ブラウザのデベロッパー ツールにこのようなツールがある場合)に注目してください。

ワークボックスに進みます。

このドキュメントでは、Service Worker の API と関連 API の確認をまとめます。Service Worker を直接使用して Workbox を操作する方法について、十分に学習しました。