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

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

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

キャッシュ戦略

組み込みのキャッシュ戦略のいずれかを使用して、アセットのほとんどのルートを処理できます。詳細についてはこのドキュメントですでに説明しましたが、ここではその一部をまとめます。

  • 再検証中に失効する: リクエストに対してキャッシュされたレスポンス(利用可能な場合)を使用し、ネットワークからのレスポンスによってバックグラウンドでキャッシュを更新します。そのため、アセットがキャッシュに保存されていない場合、ネットワーク レスポンスを待ってそれを使用します。これに依存するキャッシュ エントリが定期的に更新されるため、非常に安全な戦略です。この方法の短所は、常にバックグラウンドでネットワークからアセットをリクエストすることです。
  • ネットワーク ファーストでは、まずネットワークから応答を得ようとします。レスポンスを受信すると、そのレスポンスをブラウザに渡し、キャッシュに保存します。ネットワーク リクエストが失敗した場合は、最後にキャッシュされたレスポンスが使用され、アセットへのオフライン アクセスが有効になります。
  • キャッシュ ファーストは、まずキャッシュ内のレスポンスを確認し、レスポンスがあればそれを使用します。リクエストがキャッシュにない場合は、ネットワークが使用され、有効なレスポンスはブラウザに渡される前にキャッシュに追加されます。
  • ネットワークのみでは、強制的にネットワークからレスポンスが返されます。
  • キャッシュのみは、レスポンスを強制的にキャッシュから取得します。

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

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

workbox-routing は、ルートを照合してキャッシュ戦略で処理する registerRoute メソッドを公開します。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);

複数のキャッシュの使用

ワークボックスを使用すると、バンドルされた戦略で利用可能な 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);
<ph type="x-smartling-placeholder">
</ph> Chrome の DevTools のアプリケーション タブ内にあるキャッシュ インスタンスのリストのスクリーンショット。3 つの異なるキャッシュが表示されています。1 つは「scripts」、もう 1 つは「styles」という名前、もう 1 つは「images」という名前です。
Chrome DevTools の [アプリケーション] パネルにあるキャッシュ ストレージ ビューア。アセットタイプごとのレスポンスは別々のキャッシュに保存されます。

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

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);

保存容量の上限の遵守は複雑な場合があります。ストレージを圧迫している可能性があるユーザーや、ストレージを最も効率的に使用したいユーザーを考慮することをおすすめします。Workbox の 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 です。キャッシュに保存する不透明なレスポンスの数を決定する際は、この点に留意してください。通常よりもずっと早く、ストレージ容量を超過するおそれがあるためです。