- Service Workerは「オフライン対応のためのAPI」ではありません
- SPAが抱える問題とキャッシュの必要性
- Service Workerの動作原理
- キャッシュストレージという別のキャッシュ
- SPAで使われる5つのキャッシュ戦略
- オフライン対応の本当の作り方
- よく起きる事故と注意点
- SPAとService Workerの関係
Service Workerは「オフライン対応のためのAPI」ではありません
SPA(Single Page Application)を学び始めたとき、多くの解説で「Service Worker=オフライン対応」と説明されます。確かにオフライン表示は可能になりますが、それは本質ではありません。
Service Workerの本当の役割は、ブラウザとサーバの間に“もう一つのサーバ”を作ることです。
もう少し具体的に言うと、HTTP通信をJavaScriptで制御できるようにする仕組みです。つまりService Workerはフロントエンドの機能ではなく、ネットワーク層の機能です。
これを理解すると、SPAにおけるキャッシュ戦略の意味が大きく変わります。単なる「高速化」ではなく、「通信の設計」になります。
SPAが抱える問題とキャッシュの必要性
SPAの通信はページではなくリソース単位になる
従来のWeb(MPA)では、ブラウザはページ単位で通信していました。
- HTMLを取得
- CSSを取得
- JSを取得
- ページ遷移で再取得
一方SPAではページ遷移がありません。その代わり、次のような通信が増えます。
- APIリクエスト
- JSON取得
- 画像取得
- 遅延ロードのJS
つまりSPAは「1回の大きな通信」から「大量の小さな通信」へ変わります。ここで問題が発生します。
回線が遅い環境では、毎回サーバへ取りに行くと体感速度が大きく落ちます。特にスマートフォン環境では顕著です。
この通信制御を行うのがService Workerです。
Service Workerの動作原理
ブラウザの外で動くJavaScript
Service Workerは通常のJavaScriptと違い、ページに紐付きません。ウィンドウが閉じられても存在し続けます。
登録は次のように行います。
if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js') }
この時点でブラウザは、sw.jsを「通信の代理人」として扱います。
重要なのはここからです。Service Workerはfetchイベントをフックできます。
self.addEventListener('fetch', event => { event.respondWith(fetch(event.request)) })
つまり、ブラウザの全HTTPリクエストを横取りできます。
ここがキャッシュ戦略の核心です。
キャッシュストレージという別のキャッシュ
ブラウザキャッシュとの違い
HTTPには元々キャッシュがあります。Cache-ControlやETagです。しかしSPAではそれだけでは不十分です。
理由は、HTTPキャッシュはサーバ主導だからです。
Service Workerが使うのはCache Storage APIです。
const cache = await caches.open('app-cache') await cache.put(request, response)
このキャッシュは
- 有効期限を自由に決められる
- APIレスポンスも保存できる
- プログラムで削除できる
つまり、アプリケーションレベルのキャッシュです。
SPAで使われる5つのキャッシュ戦略
Cache First
まずキャッシュを確認し、無ければ通信します。
特徴:
- 非常に高速
- オフラインで動く
- 更新が遅れる
主にJS/CSS/画像に向きます。
Network First
まず通信し、失敗したらキャッシュを使います。
特徴:
- 常に最新
- オフライン対応可能
- 初回が遅い
APIレスポンスに向きます。
Stale While Revalidate
キャッシュを即返し、裏で通信して更新します。
event.respondWith( caches.match(req).then(cacheRes => { const fetchRes = fetch(req).then(res => { cache.put(req, res.clone()) return res }) return cacheRes || fetchRes }) )
体感速度が最も良い方式で、多くのPWAが採用しています。
Network Only
常に通信。キャッシュしません。認証系APIで使います。
Cache Only
キャッシュのみ。オフライン画面などに使います。
オフライン対応の本当の作り方
よくある失敗は「オフラインページだけ作る」ことです。それではSPAは壊れます。
SPAが動くために必要なのは次です。
- index.html
- JavaScriptバンドル
- ルーティングJS
つまり「画面」ではなくアプリケーション本体をキャッシュしなければなりません。
installイベントでプリキャッシュを行います。
self.addEventListener('install', event => { event.waitUntil( caches.open('app').then(cache => { return cache.addAll([ '/', '/index.html', '/main.js', '/styles.css' ]) }) ) })
これで回線が無くてもSPAは起動します。
よく起きる事故と注意点
更新されない問題
Service Worker最大の罠は「更新されない」です。
ブラウザは安全のため、古いService Workerを残します。その結果
- 新しいJSをデプロイ
- ユーザは古いJSを実行
- API仕様がズレる
- 画面が壊れる
という事故が現実に起きます。
対策としてactivateイベントで古いキャッシュを削除します。
self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(keys => Promise.all( keys.map(key => { if (key !== 'app-v2') return caches.delete(key) }) ) ) ) })
ログイン情報をキャッシュしてしまう危険
APIレスポンスを何でもキャッシュすると、別ユーザにデータが見える可能性があります。特に
- /me
- /profile
- /cart
などのエンドポイントはキャッシュしてはいけません。
SPAとService Workerの関係
SPAの本質は「画面をダウンロードするアプリケーション」です。つまり一度取得したリソースをどれだけ再利用できるかが体験を決めます。
Service Workerは単なるPWA機能ではありません。SPAのネットワークアーキテクチャそのものです。
SPAが速いかどうかは、JavaScriptの軽さよりも、キャッシュ設計で決まることが多いです。レンダリングの最適化より先に、通信の最適化を考える方が体感速度は改善します。
最終的に重要なのは「オフラインにすること」ではありません。ユーザにネットワークの存在を意識させないことです。
Service Workerはそのための仕組みです。通信を隠蔽することで、Webは初めてアプリケーションの振る舞いに近づきます。