実行時にリソースをキャッシュする

ウェブ アプリケーションに含まれる一部のアセットは、使用頻度が低いもの、非常に大きいもの、ユーザーのデバイス(レスポンシブ画像など)や言語によって異なるものがあります。このようなケースではプレキャッシュがアンチパターンとなる可能性があるため、代わりにランタイム キャッシュを使用する必要があります。

ワークボックスでは、workbox-routing モジュールを使用してアセットのランタイム キャッシュを処理し、ルートを照合し、workbox-strategies モジュールでキャッシュ戦略を処理できます。

キャッシュ戦略

組み込みのキャッシュ戦略のいずれかを使用して、アセットのほとんどのルートを処理できます。このドキュメントの前半で詳しく説明していますが、ここでまとめて確認しましょう。

  • Stale while Revalidate は、キャッシュに保存されたレスポンス(利用可能な場合)を使用して、ネットワークからのレスポンスでバックグラウンドでキャッシュを更新します。そのため、アセットがキャッシュに保存されていない場合は、ネットワーク レスポンスを待機して使用します。これは、キャッシュ エントリに依存するキャッシュ エントリを定期的に更新するので、かなり安全な戦略です。デメリットは、常にバックグラウンドでネットワークにアセットをリクエストすることです。
  • ネットワーク ファースト: 最初にネットワークからのレスポンスの取得を試みます。レスポンスを受信すると、そのレスポンスをブラウザに渡してキャッシュに保存します。ネットワーク リクエストが失敗した場合、キャッシュに保存された最後のレスポンスが使用されるため、アセットへのオフライン アクセスが可能になります。
  • [キャッシュ ファースト] では、まずレスポンスのキャッシュがチェックされ、可能な場合はそれを使用します。リクエストがキャッシュにない場合は、ネットワークが使用され、有効なレスポンスがすべてキャッシュに追加されてから、ブラウザに渡されます。
  • ネットワークのみ: ネットワークから強制的にレスポンスを受信します。
  • [Cache Only] は、レスポンスをキャッシュから取得します。

これらの戦略は、workbox-routing が提供するメソッドを使用して、一部のリクエストに適用できます。

ルート マッチングによるキャッシュ戦略の適用

workbox-routingregisterRoute メソッドを公開し、ルートを照合してキャッシュ戦略でルートを処理します。registerRouteRoute オブジェクトを受け入れ、次に 2 つの引数を受け入れます。

  1. ルートの一致条件を指定する文字列、正規表現、または一致コールバック
  2. ルートのハンドラ。通常は workbox-strategies によって提供される戦略です。

一致コールバックは、Request オブジェクト、リクエスト URL 文字列、フェッチ イベント、リクエストが同一オリジン リクエストかどうかのブール値を含むコンテキスト オブジェクトを提供するため、ルートのマッチングに適しています。

次に、一致したルートをハンドラが処理します。次の例では、送られてきた同一オリジンの画像リクエストに一致する新しいルートが作成され、先にキャッシュを適用し、ネットワーク戦略にフォールバックします。

// sw.js
import { registerRoute, Route } from 'workbox-routing';
import { CacheFirst } from 'workbox-strategies';

// A new route that matches same-origin image requests and handles
// them with the cache-first, falling back to network strategy:
const imageRoute = new Route(({ request, sameOrigin }) => {
  return sameOrigin && request.destination === 'image'
}, new CacheFirst());

// Register the new route
registerRoute(imageRoute);

複数のキャッシュの使用

Workbox では、バンドル戦略で使用可能な cacheName オプションを使用して、キャッシュに保存されたレスポンスを別々の Cache インスタンスに分割できます。

次の例では、画像で stale-while-revalidate 戦略が採用されていますが、CSS アセットと JavaScript アセットでは、ネットワーク戦略へのキャッシュ ファーストのフォールバックが使用されます。cacheName プロパティを追加することにより、各アセットのルートによって、レスポンスが別々のキャッシュに保存されます。

// sw.js
import { registerRoute, Route } from 'workbox-routing';
import { CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';

// Handle images:
const imageRoute = new Route(({ request }) => {
  return request.destination === 'image'
}, new StaleWhileRevalidate({
  cacheName: 'images'
}));

// Handle scripts:
const scriptsRoute = new Route(({ request }) => {
  return request.destination === 'script';
}, new CacheFirst({
  cacheName: 'scripts'
}));

// Handle styles:
const stylesRoute = new Route(({ request }) => {
  return request.destination === 'style';
}, new CacheFirst({
  cacheName: 'styles'
}));

// Register routes
registerRoute(imageRoute);
registerRoute(scriptsRoute);
registerRoute(stylesRoute);
Chrome の DevTools の [Application] タブに表示されたキャッシュ インスタンスのリストのスクリーンショット。3 つの異なるキャッシュが表示されています。1 つは「scripts」、もう 1 つは「styles」、最後のキャッシュは「images」という名前です。
Chrome DevTools の [Application] パネルにあるキャッシュ ストレージ ビューア。さまざまなアセットタイプに対するレスポンスは、別々のキャッシュに保存されます。

キャッシュ エントリの有効期限を設定する

Service Worker のキャッシュを管理する場合は、ストレージの割り当てに注意してください。ExpirationPlugin はキャッシュのメンテナンスを簡素化し、workbox-expiration によって公開されます。これを使用するには、キャッシュ戦略の構成でこれを指定します。

// sw.js
import { registerRoute, Route } from 'workbox-routing';
import { CacheFirst } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';

// Evict image cache entries older thirty days:
const imageRoute = new Route(({ request }) => {
  return request.destination === 'image';
}, new CacheFirst({
  cacheName: 'images',
  plugins: [
    new ExpirationPlugin({
      maxAgeSeconds: 60 * 60 * 24 * 30,
    })
  ]
}));

// Evict the least-used script cache entries when
// the cache has more than 50 entries:
const scriptsRoute = new Route(({ request }) => {
  return request.destination === 'script';
}, new CacheFirst({
  cacheName: 'scripts',
  plugins: [
    new ExpirationPlugin({
      maxEntries: 50,
    })
  ]
}));

// Register routes
registerRoute(imageRoute);
registerRoute(scriptsRoute);

保存容量の割り当ての遵守は複雑な場合があります。ストレージの負荷に直面している可能性があるユーザーや、ストレージを最も効率的に使用したいユーザーを考慮することをおすすめします。ワークボックスの ExpirationPlugin ペアは、この目標を達成するために役立ちます。

クロスオリジンの考慮事項

Service Worker とクロスオリジン アセットとのインタラクションは、同一オリジン アセットとは大きく異なります。クロスオリジン リソース シェアリング(CORS)は複雑で、Service Worker でクロスオリジン リソースを処理する方法もその複雑さに直面しています。

不透明なレスポンス

no-cors モードでクロスオリジン リクエストを行う場合、レスポンスは Service Worker のキャッシュに保存され、ブラウザで直接使用することもできます。ただし、レスポンスの本文自体を JavaScript で読み取ることはできません。これは不透明レスポンスと呼ばれます。

不透明なレスポンスは、クロスオリジン アセットの検査を防ぐことを目的としたセキュリティ対策です。クロスオリジン アセットのリクエストは引き続き行えます。また、キャッシュに保存することさえ可能ですが、単にレスポンスの本文やステータス コードを読み取ることはできません。

CORS モードを有効にする

レスポンスの読み取りを許可する制限の緩い CORS ヘッダーを設定したクロスオリジン アセットを読み込んでも、クロスオリジン レスポンスの本文は不透明になることがあります。たとえば、次の HTML は no-cors リクエストをトリガーします。そのため、CORS ヘッダーの設定にかかわらず、不透明なレスポンスが返されます。

<link rel="stylesheet" href="https://example.com/path/to/style.css">
<img src="https://example.com/path/to/image.png">

不透明でないレスポンスを返す cors リクエストを明示的にトリガーするには、HTML に crossorigin 属性を追加して、明示的に CORS モードを有効にする必要があります。

<link crossorigin="anonymous" rel="stylesheet" href="https://example.com/path/to/style.css">
<img crossorigin="anonymous" src="https://example.com/path/to/image.png">

Service Worker のルートが実行時にサブリソースをキャッシュ保存する場合、この点に注意してください。

ワークボックスで不透明なレスポンスがキャッシュに保存されない

デフォルトでは、Workbox は不透明なレスポンスを慎重にキャッシュに保存します。レスポンス コードで不透明なレスポンスを調べることは不可能であるため、キャッシュ ファーストまたはキャッシュのみの戦略を使用している場合、エラー レスポンスをキャッシュに保存すると、エクスペリエンスが永続的に機能しなくなる可能性があります。

Workbox で不透明なレスポンスをキャッシュに保存する必要がある場合は、ネットワーク ファーストまたは stale-while-validate のいずれかの戦略を使用して処理する必要があります。はい。つまり、アセットは毎回ネットワークからリクエストされます。ただし、レスポンスの失敗が残らず、最終的に使用可能なレスポンスに置き換えられます。

別のキャッシュ戦略を使用していて不透明なレスポンスが返されると、開発モードではレスポンスがキャッシュされていないという警告が Workbox から表示されます。

不透明なレスポンスを強制的にキャッシュに保存する

キャッシュ ファーストまたはキャッシュのみの戦略で不透明なレスポンスをキャッシュに保存する必要があることが確実にある場合、workbox-cacheable-response モジュールを使用して Workbox に強制的にキャッシュに保存できます。

import {Route, registerRoute} from 'workbox-routing';
import {NetworkFirst, StaleWhileRevalidate} from 'workbox-strategies';
import {CacheableResponsePlugin} from 'workbox-cacheable-response';

const cdnRoute = new Route(({url}) => {
  return url === 'https://cdn.google.com/example-script.min.js';
}, new CacheFirst({
  plugins: [
    new CacheableResponsePlugin({
      statuses: [0, 200]
    })
  ]
}))

registerRoute(cdnRoute);

不透明なレスポンスと navigator.storage API

クロスドメイン情報の漏洩を避けるため、保存容量の上限の計算に使用される不透明レスポンスのサイズに大幅なパディングが追加されます。これは、navigator.storage API が保存容量を報告する方法に影響します。

このパディングはブラウザによって異なりますが、Chrome の場合、1 つのキャッシュに保存された不透明なレスポンスが、使用される全体的な保存容量に占める最小サイズは約 7 MB です。キャッシュに保存する不透明なレスポンスの数を決定する際は、この点に留意してください。想定よりもずっと早く保存容量の上限を超えてしまう可能性があるためです。