#はじめに
「電波が届かない場所でもWebアプリを使いたい」——Service Workerを使えば、これが可能になります。
Service Workerは、ブラウザとネットワークの間に立つプログラム可能なプロキシです。リクエストをインターセプトし、キャッシュから返したり、ネットワークに転送したりを自由に制御できます。
この記事を読むと、以下のことができるようになります:
- Service Workerの基本的な仕組みを理解できる
- Cache APIを使ってキャッシュを操作できる
- オフライン対応の戦略を選択できる
#Service Workerとは
Service Workerは、Webページとは別のスレッドで動作し、ネットワークリクエストをインターセプトできるJavaScriptファイルです。
[Webページ] ←→ [Service Worker] ←→ [ネットワーク / キャッシュ]
#特徴
- バックグラウンド動作: ページが閉じていても動作可能
- プログラム可能: キャッシュ戦略を自由に実装
- HTTPS必須: セキュリティ上、HTTPSでのみ動作(localhost除く)
- 非同期: すべての処理がPromiseベース
#Service Workerのライフサイクル
#1. 登録(Registration)
メインページからService Workerを登録します。
// main.js
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW registered:', registration.scope);
})
.catch(error => {
console.error('SW registration failed:', error);
});
}
#2. インストール(Install)
初回登録時、または更新時に実行されます。
// sw.js
self.addEventListener('install', event => {
console.log('SW installing...');
// 事前キャッシュ
event.waitUntil(
caches.open('v1').then(cache => {
return cache.addAll([
'/',
'/index.html',
'/style.css',
'/app.js',
]);
})
);
});
#3. 有効化(Activate)
インストール後、古いService Workerが制御していたページがすべて閉じられると有効化されます。
self.addEventListener('activate', event => {
console.log('SW activated');
// 古いキャッシュの削除
event.waitUntil(
caches.keys().then(keys => {
return Promise.all(
keys.filter(key => key !== 'v1')
.map(key => caches.delete(key))
);
})
);
});
#4. フェッチ(Fetch)
ページからのリクエストをインターセプトします。
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});
#Cache API
Service Worker内でキャッシュを操作するためのAPIです。
#キャッシュを開く
const cache = await caches.open('my-cache-v1');
#リクエストをキャッシュに追加
// 単一のリクエスト
await cache.add('/style.css');
// 複数のリクエスト
await cache.addAll(['/index.html', '/app.js', '/style.css']);
#カスタムレスポンスをキャッシュ
const response = await fetch('/data.json');
await cache.put('/data.json', response);
#キャッシュからの取得
const response = await cache.match('/style.css');
if (response) {
// キャッシュヒット
} else {
// キャッシュミス
}
#キャッシュの削除
await cache.delete('/old-file.js');
await caches.delete('old-cache-v1');
#キャッシュ戦略
#1. Cache First(キャッシュ優先)
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cached => {
return cached || fetch(event.request);
})
);
});
- まずキャッシュを確認
- なければネットワークから取得
- 適用場面: 静的アセット、変更頻度の低いリソース
#2. Network First(ネットワーク優先)
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request)
.catch(() => caches.match(event.request))
);
});
- まずネットワークから取得を試みる
- 失敗したらキャッシュを使用
- 適用場面: 最新データが重要なAPI、ニュース
#3. Stale-While-Revalidate
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cached => {
const fetchPromise = fetch(event.request).then(response => {
caches.open('dynamic').then(cache => {
cache.put(event.request, response.clone());
});
return response;
});
return cached || fetchPromise;
})
);
});
- キャッシュがあれば即座に返す
- 同時にネットワークから取得してキャッシュを更新
- 適用場面: アバター画像、ソーシャルフィード
#4. Cache Only
self.addEventListener('fetch', event => {
event.respondWith(caches.match(event.request));
});
- キャッシュのみ使用
- 適用場面: オフライン専用アプリ、事前キャッシュされたリソース
#5. Network Only
self.addEventListener('fetch', event => {
event.respondWith(fetch(event.request));
});
- 常にネットワークから取得
- 適用場面: 分析リクエスト、非GETリクエスト
#戦略の使い分け
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
if (url.pathname.startsWith('/api/')) {
// API: Network First
event.respondWith(networkFirst(event.request));
} else if (url.pathname.match(/\.[a-f0-9]{8}\./)) {
// ハッシュ付きアセット: Cache First
event.respondWith(cacheFirst(event.request));
} else if (url.pathname.endsWith('.html')) {
// HTML: Network First + Offline Fallback
event.respondWith(networkFirstWithFallback(event.request));
}
});
#Service Workerの更新
#更新の検出
ブラウザは登録されたService Workerファイルを定期的にチェックし、バイト単位で異なれば新しいバージョンをインストールします。
#更新フロー
1. 新しいsw.jsがダウンロードされる
2. installイベントが発火(新バージョン)
3. 古いSWがまだアクティブ
4. すべてのタブが閉じられる
5. activateイベントが発火(新バージョン)
6. 新しいSWがアクティブに
#即座に有効化
self.addEventListener('install', event => {
self.skipWaiting(); // 待機をスキップ
});
self.addEventListener('activate', event => {
event.waitUntil(clients.claim()); // 既存のクライアントを制御
});
注意: 破壊的な変更がある場合、古いページと新しいService Workerの不整合が起きる可能性があります。
#DevToolsでの確認
#Application > Service Workers
- 登録状態
- ライフサイクル状態
- 更新、停止、削除
#Application > Cache Storage
- キャッシュ名の一覧
- キャッシュ内のリソース
- 手動での削除
#オフラインテスト
- Network タブで「Offline」をチェック
- ページをリロード
- オフライン対応が機能しているか確認
#Workbox
Workboxは、Service Workerのキャッシュ戦略を簡単に実装するためのライブラリです。
#使用例
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
// 画像: Cache First
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({ cacheName: 'images' })
);
// API: Network First
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({ cacheName: 'api' })
);
// CSS/JS: Stale While Revalidate
registerRoute(
({ request }) => request.destination === 'style' || request.destination === 'script',
new StaleWhileRevalidate({ cacheName: 'static' })
);
ビルドツール連携で事前キャッシュも自動化できます。
#まとめ
- Service Workerはネットワークリクエストをインターセプトするプロキシ
- Cache APIでプログラム的にキャッシュを制御
- 主な戦略: Cache First、Network First、Stale-While-Revalidate
- 用途に応じて戦略を使い分ける
- Workboxを使うと実装が簡単に
- 更新戦略を慎重に設計する
#次のステップ
キャッシュ戦略モジュールはこれで完了です。HTTPキャッシュの基本、Cache-Controlディレクティブ、条件付きリクエスト、CDNキャッシュ、キャッシュバスティング、そしてService Workerについて学びました。
次のモジュールでは、パフォーマンスと配信について学びます。DNS、CDN、圧縮、画像最適化、Core Web Vitalsなど、Webサイトを高速化するための技術を紹介します。