web-pwa-service-workers について
Service Worker を使う Web 開発者向けの実践パターン集です。ライフサイクル、コンテンツ別キャッシュ、オフライン時の代替応答、ユーザー主導の更新を扱います。
主な機能
- 01Service Worker のライフサイクル処理
- 02コンテンツ別キャッシュ戦略
- 03フェッチのルーティングとオフライン対応
- 04バージョン別キャッシュの整理
- 05更新の検出と適用
活用シーン
- 01アプリシェルの事前キャッシュ
- 02ナビゲーションのネットワーク優先設定
- 03切断時のオフラインページ表示
- 04待機中の Worker 更新の提示
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ agents-inc/ skills --skill web-pwa-service-workersインストール後、Claude Code で/web-pwa-service-workersを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
metadata.yamlreference.mdexamples/caching.mdcore.mdupdates.md詳細
Service Worker のパターン
クイックガイド: Service Worker は、独自のライフサイクルを持つプログラム可能なネットワークプロキシです。 install で事前キャッシュし、activate でクリーンアップし、fetch をインターセプトします。コンテンツに合ったキャッシュ戦略を選びましょう。ハッシュ付きアセットにはキャッシュ優先、HTML にはネットワーク優先、重要度の低い API の読み取りには stale-while-revalidate を使います。多くのバグの原因となるのは、次の 2 点です。レスポンス本文を読み取れるのは 1 回だけなので、
cache.put(request, response.clone())とします。また、新しい Service Worker は、古い Service Worker を使っているすべてのタブが閉じるまで待機するため、更新には明示的な検知と、ユーザーが操作するskipWaitingが必要です。
詳細リソース:
- examples/core.md — 登録、ライフサイクル全体のテンプレート、4 つの戦略の実装、メッセージの種類
- examples/caching.md — バックグラウンド更新、有効期限のタイムスタンプ、エンドポイントごとのルーティング、ナビゲーションプリロード、クォータのクリーンアップ
- examples/updates.md — バージョンメッセージング、積極的/延期/アイドル時の更新、段階的なロールアウト、キャッシュの移行
- reference.md — コンテンツタイプ別の戦略表、ライフサイクルと Cache API の参照、更新戦略の選択、レビュー用チェックリスト
該当するパス
- 事前キャッシュするアプリシェル — HTML はキャッシュされた 1 つのドキュメントで、ルーティングはクライアント側で行われます。キャッシュ優先で配信し、ナビゲーションプリロードは省略します。プリロードするものがありません。
- サーバー側レンダリングまたは認証が必要なページ — ナビゲーションのたびに新しいドキュメントが読み込まれます。タイムアウトとオフラインフォールバックを備えたネットワーク優先で配信し、Service Worker が起動する前にリクエストを開始できるよう、ナビゲーションプリロードを有効にします。 examples/caching.md のパターン 8 に従ってください。
<critical_requirements>
Service Worker のコードを書く前に
非同期のライフサイクルタスクはすべて event.waitUntil() で囲みます。 Promise が解決するまで Service Worker を稼働状態に保ちます。これがないと、事前キャッシュの途中でブラウザーが終了させる可能性があります。
すべてのキャッシュ名にバージョンを含め、activate 中に現行以外のキャッシュを削除します。 これによりアップグレードが適切に行われ、ストレージが際限なく増えるのを防げます。
キャッシュする前に複製します — cache.put(request, response.clone())。 レスポンス本文を消費できるのは 1 回だけなので、元のレスポンスをキャッシュするとクライアントに空のレスポンスが返されます。
待機中の Service Worker をいつ切り替えるかは、クライアントに決めてもらいます。 待機中の Service Worker を検知してユーザーに知らせ、そのメッセージに応じて skipWaiting() を呼び出します。これにより、開いているセッション中に動作が予期せず変わることはありません。
すべての fetch の経路にフォールバックを用意します。 ナビゲーション用の事前キャッシュ済み offline.html と、最後の手段として構築する Response により、ネットワーク障害時に自分で用意したページを表示できます。
</critical_requirements>
自動検出: navigator.serviceWorker、serviceWorker.register、ServiceWorkerGlobalScope、sw.js、sw.ts、self.skipWaiting、clients.claim、event.waitUntil、event.respondWith、event.preloadResponse、caches.open、caches.match、cache.addAll、CacheStorage、navigationPreload、updateViaCache、controllerchange、updatefound、precache
対象:
- ワーカースレッドからネットワークリクエストをインターセプトして応答する
- コンテンツタイプごとにキャッシュ戦略を選び、実装する
- アプリシェルを事前キャッシュし、置き換えられたキャッシュをクリーンアップする
- 待機中の Service Worker を検知し、ユーザーの希望に沿って更新を適用する
- キャッシュとネットワークの両方が失敗した場合に、オフライン用のフォールバックを配信する
このドキュメントでは扱わない内容:
- アプリケーションが所有し書き込む、構造化されたローカルデータ — レスポンスキャッシュにはサーバーが返した内容が保存されますが、これはユーザーがオフラインで編集するレコードとは異なるライフサイクルです
- オフライン中に行われた変更をキューに入れ、後で同期すること
- プッシュ通知のコンテンツと権限のフロー — Service Worker の
pushイベントは配信のフックであり、何を表示するかは製品上の判断です
## 基本理念
サービスワーカーはプラグインではなくプロキシです。インストールされると、そのスコープ内のすべてのリクエストを監視し、応答できなかったリクエストは失敗させてしまいます。
ライフサイクルは、2つのバージョンが同時に動作するのを防ぐためにあります。新しいワーカーはすぐにインストールされますが、古いワーカーが制御するすべてのクライアントがなくなるまで待機します。そのため、1つのページで古いアセットの半分と新しいアセットの半分が混在することはありません。
Registration → Download → Install → Waiting → Activate → Fetch
↓ ↓
(skipWaiting) (claim)
skipWaiting() と clients.claim() は、この保証を回避する2つの手段です。どちらも、理由があってオプトイン方式になっています。ユーザーが更新を求めた場合や、セッション中の変更に見合うほど修正を急ぐ必要がある場合に利用してください。
基本パターン
パターン 1:更新検出を伴う登録
機能を検出してから登録し、定期的に更新を確認し、待機中のワーカーを追跡して UI から更新を提示できるようにします。
const registration = await navigator.serviceWorker.register("/sw.js", {
scope: "/",
updateViaCache: "none", // ask the server for the worker script every time
});
setInterval(() => registration.update(), UPDATE_CHECK_INTERVAL_MS);
registration.addEventListener("updatefound", () => {
const installing = registration.installing;
installing?.addEventListener("statechange", () => {
const isWaiting =
installing.state === "installed" && navigator.serviceWorker.controller;
if (isWaiting) notifyUpdateAvailable();
});
});
全コード:examples/core.md
パターン 2:ライフサイクルハンドラー
install でプリキャッシュし、activate で置き換えられたキャッシュを削除します。また、skipWaiting は無条件に呼び出すのではなく、メッセージとして受け取ります。
self.addEventListener("install", (event: ExtendableEvent) => {
event.waitUntil(
caches.open(CACHES.static).then((cache) => cache.addAll(PRECACHE_URLS)),
);
});
self.addEventListener("activate", (event: ExtendableEvent) => {
event.waitUntil(
(async () => {
const names = await caches.keys();
const current: string[] = Object.values(CACHES);
await Promise.all(
names.filter((n) => !current.includes(n)).map((n) => caches.delete(n)),
);
await self.clients.claim();
})(),
);
});
self.addEventListener("message", (event: ExtendableMessageEvent) => {
if (event.data?.type === "SKIP_WAITING") self.skipWaiting();
});
全コード:examples/core.md
パターン 3:キャッシュ戦略
ほとんどのケースは、4つの戦略で対応できます。コンテンツごとの使い分けについては、reference.md を参照してください。
| 戦略 | 動作 |
|---|---|
| Cache-first | キャッシュしたコピーを返し、なければネットワークにフォールバックします |
| Network-first | ネットワークとタイムアウトを競わせ、その後キャッシュします |
| Stale-while-revalidate | キャッシュしたコピーを返し、バックグラウンドで更新します |
| Cache-only / network-only | 1つのソースのみを使用し、フォールバックはありません |
どの実装でも、キャッシュする前に response.ok を確認し、cache.put の前にレスポンスを複製し、エントリ数に上限を設けています。
const networkResponse = await fetch(request);
if (networkResponse.ok) {
cache.put(request, networkResponse.clone()); // the clone is cached
}
return networkResponse; // the original goes to the client
全コード:examples/core.md。有効期限とエンドポイントごとのバリエーションについては、examples/caching.md を参照してください。
パターン 4:フェッチのルーティング
URL だけでなく、リクエストの形式に基づいてルーティングします。request.mode、request.destination、パス名は、それぞれ異なる情報を示します。respondWith を呼び出さずに戻ると、ブラウザーが通常どおりリクエストを処理します。
self.addEventListener("fetch", (event: FetchEvent) => {
const { request } = event;
const url = new URL(request.url);
if (request.method !== "GET") return;
if (url.origin !== location.origin) return;
if (request.mode === "navigate") {
event.respondWith(networkFirst(request, CACHES.pages));
} else if (request.destination === "image") {
event.respondWith(cacheFirstWithLimit(request, CACHES.images, MAX_IMAGES));
} else if (url.pathname.startsWith("/api/")) {
event.respondWith(staleWhileRevalidate(request, CACHES.api));
} else {
event.respondWith(cacheFirst(request, CACHES.static));
}
});
全コード:examples/core.md
パターン 5:オフラインフォールバック
install でオフラインページをプリキャッシュし、ナビゲーション時にキャッシュにもネットワークにも対象がない場合に、そのページを返します。最後の手段として Response を構築することで、ブラウザー独自のエラーページに行き着く経路をなくします。
if (request.mode === "navigate") {
const offlinePage = await caches.match("/offline.html");
if (offlinePage) return offlinePage;
}
return new Response("<h1>Offline</h1><p>Check your connection.</p>", {
status: 503,
headers: { "Content-Type": "text/html" },
});
全コード:examples/core.md
パターン 6:ナビゲーションプリロード
ワーカーの起動には、デスクトップで約50ミリ秒、低速なスマートフォンでは250ミリ秒以上かかります。ネットワーク優先のナビゲーションでは、リクエストが始まる前にこの時間がかかります。ナビゲーションプリロードを使うと、起動と並行してリクエストを開始できます。
// activate
if (self.registration.navigationPreload) {
await self.registration.navigationPreload.enable();
}
// fetch
const preloadResponse = await event.preloadResponse;
if (preloadResponse) {
cache.put(event.request, preloadResponse.clone());
return preloadResponse;
}
有効にしたら、event.preloadResponse を使用してください。代わりに fetch(event.request) を呼び出すと、同じリソースに対してネットワークリクエストが2回発生します。
全コード:examples/caching.md
パターン 7:ユーザーが制御する更新
待機中のワーカーをユーザーに提示し、ユーザーが承認したら SKIP_WAITING を送信します。また、controllerchange で1回だけ再読み込みを行い、ページとワーカーの状態を一致させます。
if (registration.waiting) showUpdateBanner();
function applyUpdate() {
registration.waiting?.postMessage({ type: "SKIP_WAITING" });
}
let refreshing = false;
navigator.serviceWorker.addEventListener("controllerchange", () => {
if (refreshing) return;
refreshing = true;
window.location.reload();
});
全コード:examples/updates.md — バージョンメッセージング、適用の延期とアイドル時の適用、段階的なロールアウト、キャッシュの移行
<red_flags>
問題になりやすい点
実行時に問題が発生します:
- install または activate で
event.waitUntil()を使わずに非同期処理を行うと、タスクの途中でブラウザーがワーカーを終了する可能性があります。プロミスをラップしてください .clone()を使わずにcache.put(request, response)を呼び出すと、本文が消費され、クライアントには何も届きません。複製をキャッシュし、元のレスポンスを返してくださいresponse.okを確認せずにキャッシュすると、404 や 500 が保存され、キャッシュが有効な間ずっとそのレスポンスが返されます。putの前に条件を確認してください- バージョンなしのキャッシュ名を使うと、activate 時に現在のキャッシュと古いキャッシュを区別できません。バージョン定数を埋め込んでください
- ナビゲーション用の
offline.htmlがないと、ユーザーにはブラウザーのエラーページが表示されます。install 時に事前キャッシュし、fetch のエラー処理で返してください - ナビゲーションプリロードが有効なワーカーで
fetch(event.request)を使うと、ナビゲーションのたびにネットワークリクエストが 2 回発生します。event.preloadResponseを使用してください - POST リクエストをキャッシュしようとすると、Cache API は GET リクエストをキーにするため失敗します。
request.method !== "GET"の場合は早期に戻ってください
予想外の動作:
- スコープはスクリプトの配置場所に従います。
/sw.jsは/を制御しますが、/scripts/sw.jsが制御するのは/scripts/だけです - Service Worker には HTTPS が必要ですが、開発時の localhost は例外です
- アイドル状態のワーカーは終了後に再起動するため、モジュールレベルの状態はイベント間で保持されません
clients.claim()は再読み込みせずに制御を取得するため、開いたページでは古い HTML と新しいワーカーが共存します- ワーカースクリプトのバイト単位の変更だけが更新を引き起こします。Web アプリマニフェストや事前キャッシュされたアセットを編集しても更新されません
- DevTools の「Update on reload」は待機フェーズを省略するため、開発時には更新処理が正しく見えても、本番環境では失敗します
- 不透明なクロスオリジンレスポンスは、実際のサイズをはるかに上回る水増しされたサイズでストレージ容量を消費します
- タイムアウトのないネットワーク優先の処理は、接続が技術的には確立されているものの応答しない場合、無期限に停止します