Service Worker
ブラウザとネットワークの間でプロキシとして動作し、オフライン対応やキャッシュ制御を実現する Web API
Service Worker とは
Service Worker は、ブラウザとネットワークの間でプロキシとして動作する JavaScript で、ネットワークリクエストのインターセプト、キャッシュ制御、オフライン対応、プッシュ通知を実現する。PWA (Progressive Web App) の基盤技術だ。
ライフサイクル
登録からリクエストを横取りできるようになるまでに、インストールと待機とアクティベートの 3 段を通る。待機が入るのが Service Worker の癖で、更新版はスクリプトが 1 バイトでも違えば別物として扱われ、既存の版と並んで起動して自分の install イベントを受けるが、既存の版が制御しているページが 0 になるまでアクティベートされない。
1. 登録 (register)
navigator.serviceWorker.register('/sw.js')
2. インストール (install)
→ 静的アセットを先回りしてキャッシュへ
→ waitUntil に渡した Promise が失敗すればこの版は破棄され、
既存の版が動き続ける
3. 待機 (waiting) ※更新時のみ
→ 旧版が制御するページが 0 になるまで足止め
→ self.skipWaiting() で飛ばせる
4. アクティベート (activate)
→ 不要になった世代のキャッシュを削除
5. フェッチ (fetch)
→ スコープ内のリクエストを横取り
リロードでは旧版のページと新版のページが一瞬重なるため、タブを 1 回再読み込みしても待機は解けない。すべてのタブを閉じるか、self.skipWaiting() を書いて明示的に飛ばす。
登録とスコープ
登録は機能の有無を確認してから行う。古いブラウザや安全なコンテキストでない場合に navigator.serviceWorker 自体が存在しないため、参照する前に判定する。
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
どのページを制御するかは、スクリプトの置き場所で決まる。既定のスコープはスクリプトが置かれたディレクトリで、/sw.js ならオリジン全体、/assets/sw.js なら /assets/ 以下だけになる。スコープはサイトのベース URL からの相対で解決されるため、登録を呼んだページの場所には左右されない。ディレクトリより上を制御したいときは、スクリプトの応答に Service-Worker-Allowed ヘッダーを付けて許可する必要がある。ビルド出力を /assets/ などへ固めている構成でスクリプトも一緒に置いてしまい、トップページが制御されないまま「オフラインで動かない」となるのは、この規則の見落としが原因になりやすい。
キャッシュ戦略
選び分けの軸は、古い内容を返してしまった場合の害の大きさである。内容が変わればファイル名も変わるビルド済みアセットは古い応答を返す危険がないので Cache First で良く、価格や在庫のように古い値が誤情報になるものはネットワークを先に試す。HTML を Cache First にすると、新しい版をデプロイしても手元のキャッシュが返り続けて更新が止まるため避ける。
| 戦略 | 説明 | 用途 |
|---|---|---|
| Cache First | キャッシュを優先、なければネットワーク | 静的アセット (CSS, JS, 画像) |
| Network First | ネットワークを優先、失敗したらキャッシュ | API レスポンス |
| Stale While Revalidate | キャッシュを返しつつ、バックグラウンドで更新 | 頻繁に更新されるデータ |
Cache First の実装
install での書き込みは event.waitUntil() に渡す。渡さないとインストール完了と見なされて処理中に停止し得る。addAll は 1 つでも取得に失敗すると全体が失敗し、この版は破棄される。キャッシュ名に世代を持たせ、activate で不要な世代を消すところまでを一組にする。
// sw.js
const CACHE_NAME = 'v1';
const PRECACHE_URLS = ['/index.html', '/styles.css', '/app.js'];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((names) =>
Promise.all(names.filter((n) => n !== CACHE_NAME).map((n) => caches.delete(n)))
)
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => cached || fetch(event.request))
);
});
Workbox による記述
戦略ごとの分岐や世代の後始末を自分で書くと、条件の抜けがそのままオフライン時の白画面になる。Workbox はこの定型を関数として提供し、ビルド時に差し込まれるプリキャッシュ一覧 (self.__WB_MANIFEST) とリクエストの条件を組み合わせて宣言的に書ける。
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst } from 'workbox-strategies';
precacheAndRoute(self.__WB_MANIFEST);
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({ cacheName: 'images' })
);
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({ cacheName: 'api' })
);
オフライン時の代替ページ
代替ページを返す対象は、ページ遷移のリクエストだけに絞る。すべての失敗に HTML を返すと、画像や JavaScript の取得失敗にも HTML が渡り、読み込みエラーが分かりにくい形で表に出る。
self.addEventListener('fetch', (event) => {
if (event.request.mode !== 'navigate') return;
event.respondWith(
fetch(event.request).catch(() => caches.match('/offline.html'))
);
});
注意点
- 安全なコンテキストでのみ動作する。開発用に
http://localhostは例外として扱われる - DOM にアクセスできない (Web Worker と同様)。使えるのは
fetch、Cache API、IndexedDB、postMessageなど - 登録した直後のページは、そのページ自身の取得が Service Worker を通っていないため制御下に入らない。効果を見るには再読み込みが必要で、
clients.claim()を書けば制御下にない既存のページも引き取れる - 更新版は待機に入るため、デプロイしただけでは切り替わらない (
self.skipWaiting()を書くか、全タブを閉じてもらう)
つまずきやすい点
self.skipWaiting() は更新の遅れを解決する代わりに、開いているページの下でキャッシュの世代を入れ替える。すでに読み込まれた HTML が、遅延読み込みで新しい世代のファイルを取りに行って 404 になる事故はこれで起きる。ファイル名に内容の識別子が入っていない構成や、遷移のたびに追加のスクリプトを取りに行く構成では、待機を飛ばすかどうかを設計として決める必要がある。
もう一つは、スクリプト自身のキャッシュである。/sw.js が長い有効期限で配信されていると、更新版の存在にブラウザが気付くまで時間がかかる。仕様上は取得時に一定期間を超えたキャッシュは使われないが、途中の配信層が返し続ける場合があるため、スクリプトの応答は短い有効期限で配る。
PWA、キャッシュ、Web Worker と合わせて押さえると、ブラウザ側で完結する処理の範囲が見えてくる。
この記事は役に立ちましたか?
関連用語
PWA
Service Worker やマニフェストを活用し、Web 技術でオフライン対応やプッシュ通知などネイティブアプリに近い体験を提供するアプローチ
Web Worker
ブラウザのメインスレッドとは別のスレッドで JavaScript を実行し、UI のブロッキングを防ぐ仕組み
キャッシュ
頻繁にアクセスされるデータを高速なストレージに保存し、読み取り性能を向上させる手法
CSP
Content Security Policy の略で、Web ページで実行可能なリソースの出所を制限するセキュリティ機構
キャッシュ無効化とは - TTL / イベント駆動 / パージ戦略の比較
キャッシュ無効化は古くなったキャッシュデータを最新に更新する仕組み。TTL 方式 / Write-through / イベント駆動パージの使い分けと実装パターンを解説
TTL
データやキャッシュの有効期限を設定し、自動的に削除 / 更新する仕組み
関連する記事
ネットワーク本ガイド - TCP/IP の教科書からプロトコル各論や実務書までの選び方
ネットワークを学ぶ技術書の選び方を 3 段階 (全体像の教科書 → HTTP や DNS のプロトコル各論 → 実務と運用) で整理。マスタリング TCP/IP や図解入門 TCP/IP などの定番書の使い分けと、ネットワーク本の賞味期限の考え方を解説します。
OS / 低レイヤー本ガイド - コンピュータの仕組みを学ぶ技術書の選び方
OS、コンパイラ、ネットワークなど低レイヤーを学べる技術書の 4 ジャンルと、どこから始めるべきかの指針、賞味期限の見極め方を紹介します。
技術書がエンジニアのキャリアを変える - 読書習慣と年収の関係
技術書の読書習慣がエンジニアのキャリアアップに影響する 3 つの経路と、ジュニア / ミドル / シニア各段階に応じた読書戦略を具体的に解説します。