Service Worker のキャッシュ戦略を 3 つに絞る

Web ServiceWorker PWA パフォーマンス

はじめに

Service Worker のキャッシュ戦略は、名前の付いたパターンだけでも 6 種類ほど知られている。 しかし実際のプロダクトで運用してみると、使い分けが必要なのは 3 つだけだった。 残りは組み合わせで表現できるか、そもそも選ぶべき場面がほとんどない。

この記事では、社内の管理画面(月間 40 万リクエスト)を 8 ヶ月運用した結果をもとに、 戦略を 3 つに絞った経緯と、絞ったあとに何が楽になったかをまとめる。

3 つに絞った結果

採用したのは次の 3 つ。判断は「更新の速さ」と「オフラインでの生存」のどちらを優先するかだけで決まる。

戦略 適用先 選ぶ理由
cache-first ハッシュ付き静的アセット 内容が変わればファイル名が変わるので、古い版を返す事故が起きない
network-first API レスポンス 鮮度が最優先。落ちたときだけキャッシュに退避する
stale-while-revalidate アバター・OGP 画像 多少古くても実害がなく、体感速度への寄与が大きい
迷ったら network-first にしておく。表示が遅いのは苦情で済むが、古いデータを見せるのは事故になる。

実装

ルーティングは 1 箇所に集約した。戦略ごとに fetch ハンドラを分けると、 どのリクエストがどの経路に乗るのか追えなくなる。

sw.js
// リクエストの種類から戦略を 1 つだけ選ぶ
const pickStrategy = (request) => {
  const url = new URL(request.url);

  if (url.pathname.startsWith('/assets/')) return cacheFirst;
  if (url.pathname.startsWith('/api/'))    return networkFirst;
  if (request.destination === 'image')       return staleWhileRevalidate;

  return networkFirst;
};

self.addEventListener('fetch', (event) => {
  if (event.request.method !== 'GET') return;
  event.respondWith(pickStrategy(event.request)(event.request));
});

キャッシュ名にバージョンを埋める

デプロイのたびに古いキャッシュを消したくなるが、消すのは activate の中だけに限定したほうがよい。

計測結果

絞る前後で、初回表示以外の指標を 4 週間ずつ比較した。

指標 絞る前 絞った後
再訪問時の LCP 中央値1.42 s0.86 s
キャッシュ起因の不具合報告月 3.2 件月 0.4 件
sw.js の行数418 行146 行

効果が大きかったのは速度より 不具合報告の減少だった。 戦略が 3 つなら、報告を受けた時点でどの経路を疑えばよいかが即座に決まる。

まとめ

  1. 戦略は 3 つに絞る。増やすほど障害調査の初動が遅くなる
  2. ルーティングは 1 関数に集約し、上から順に評価する
  3. 迷ったら network-first。古いデータを見せない側に倒す

このページは Chrome 拡張 copy-md の動作確認用に用意したデモ記事です。 URL の末尾に .md を付けると、同じ内容の Markdown (article.md) が返ります。