Service Worker
さーびすわーかー
ページとネットワークの間に入るプログラム可能なプロキシ。オフライン対応とキャッシュ制御の要。
概要
Service Worker は、Webページとネットワークの間に割り込んで動く JavaScript のプログラムです。ページが発行する HTTP リクエストをすべて横取り(インターセプト)できるため、「このリクエストにはキャッシュから即答する」「これはネットワークに取りに行く」「オフラインなら代替ページを返す」といった判断をコードで書ける、いわばブラウザ内蔵のプログラマブルなプロキシです。
一度インストールされるとページを閉じた後もブラウザに登録され続け、必要なときにブラウザが起動します。ページとは別のスレッドで動くため、重い処理をしてもページの描画を妨げません。その代わり DOM には一切触れず、ページとは postMessage というメッセージ交換でしか会話できない — この「ページから独立した裏方」という立ち位置が、オフライン対応・プッシュ通知・バックグラウンド同期といった「ページが開いていなくても動く」機能の土台になっています。PWA と呼ばれるアプリ的なWeb体験の心臓部でもあります。
なぜ生まれたか
Web はもともと「毎回サーバから取ってきて表示する」モデルで、ネットワークが切れた瞬間に何もできなくなるメディアでした。地下鉄で恐竜のイラストを見た経験は誰にでもあるはずです。一方ネイティブアプリは、コードもデータも端末に持っているためオフラインでも当たり前に動きます。この差を埋める最初の試みが HTML5 の AppCache(Application Cache)でしたが、キャッシュの中身をマニフェストファイルで宣言的にしか制御できず、「更新したのに古い画面が出続ける」「意図しないページまでキャッシュされる」といった挙動が開発者の制御の外で起きる、悪名高い仕様になってしまいました。
Service Worker は、この失敗から「宣言ではなくコードで制御させる」という方針転換によって2014年頃に生まれました。何をどうキャッシュし、どのリクエストにどう応答するかをすべて JavaScript で書けるようにしたことで、オフライン戦略はブラウザ任せの魔法から、開発者が設計する普通のプログラミングになったのです。なお、リクエストを横取りできる力は悪用されると危険なため、Service Worker は HTTPS でしか登録できません。
詳細
ライフサイクル — install / activate と更新の罠
Service Worker には独特のライフサイクルがあります。ページが navigator.serviceWorker.register() を呼ぶとブラウザがスクリプトを取得し、まず install イベントが走ります。ここで必要なファイルを事前キャッシュするのが定石です。続いて activate イベントで古いキャッシュの掃除などを行い、以降そのスコープ配下のページのリクエストをコントロールし始めます。
つまずきやすいのが更新の挙動です。ブラウザはページ訪問のたびにスクリプトの更新を確認し、1バイトでも違えば新しい Service Worker をインストールしますが、古い Service Worker が制御中のタブが1つでも残っている限り、新版は「待機(waiting)」状態のまま有効化されません。デプロイしたのに全タブを閉じるまで反映されない — これは事故ではなく、同じバージョンのページ群を新旧のワーカーが混在制御しないための安全設計です。急ぎたい場合は skipWaiting() で待機を飛ばせますが、稼働中のページと新ワーカーのキャッシュ内容がずれるリスクを理解した上で使う必要があります。
fetch インターセプト — プロキシとしての本領
稼働中の Service Worker は、スコープ内のページが発するあらゆるリクエスト(HTML・画像・API 呼び出しまで)で fetch イベントを受け取ります。イベントハンドラの中で respondWith() に「何を返すか」を渡すことで、キャッシュ応答・ネットワーク転送・その場で組み立てたレスポンスのいずれも自由に選べます。
保存先となるのが Cache API です。リクエストとレスポンスのペアを丸ごと保存できる、Service Worker 専用設計のストレージで、HTTP のブラウザキャッシュとは独立して、いつ入れて・いつ返し・いつ消すかを完全にコードで制御できます。
キャッシュ戦略 — どちらを先に見るか
fetch ハンドラの書き方は、実質「キャッシュとネットワークのどちらを、どんな順で見るか」の戦略選びです。代表的なのは次の3つで、リソースの性質ごとに使い分けます。
cache-first(キャッシュ優先) は、まずキャッシュを見て、あれば即座に返し、なければネットワークへ行く戦略です。ロゴやフォント、バージョン付きの JS/CSS など「変わらないもの」に最適で、体感速度が最も上がります。network-first(ネットワーク優先) はその逆で、まずネットワークを試し、失敗したときだけキャッシュにフォールバックします。ニュースフィードのような「鮮度が命だが、オフラインでも古い内容を出せた方がまし」なデータ向きです。stale-while-revalidate は折衷案で、キャッシュを即座に返しつつ、裏でネットワークから取得してキャッシュを更新します。応答は常に一瞬、内容は「次回には最新」になる — 多少古くても許されるアバター画像や一覧データで威力を発揮します。
実務では、これらの戦略やライフサイクル管理を定番パターンとして提供する Google 製ライブラリ Workbox を使うことが多く、素の API で全部書くケースは減っています。ただし「なぜその戦略か」を選ぶのは結局開発者であり、キャッシュのバージョニング(activate 時に旧世代を消す)を怠ると、直したはずのバグが古いキャッシュから蘇り続けるという典型的な罠にはまります。Service Worker は強力な分だけ、「消し方」まで設計してはじめて安全に使える技術です。