Service Worker 応用:オフラインファーストアーキテクチャとバックグラウンド同期戦略
技术架构(更新: 2026年6月4日)
Service Worker:オフラインファーストアーキテクチャの中核
Service Worker はブラウザ内でページから独立したバックグラウンドスレッドで、ネットワークリクエストの傍受、キャッシュ管理、プッシュ処理が可能です:
| 特性 | 説明 |
|---|---|
| 実行環境 | 独立 Worker スレッド、DOM アクセスなし |
| ライフサイクル | install → activate → fetch |
| スコープ | 登録パスとそのサブパス |
| 通信 | postMessage / MessageChannel |
| 永続化 | イベント駆動、メモリ常駐なし |
ライフサイクル詳解
┌──────────┐
│ parsed │
└────┬─────┘
│ install()
┌────▼─────┐
┌─────│installing│
│ └────┬─────┘
│ │ waitUntil()
│ ┌────▼─────┐
│ │ installed │ (waiting)
│ └────┬─────┘
│ │ activate()
│ ┌────▼─────┐
└────▶│activating│
└────┬─────┘
│
┌────▼─────┐
│ activated │ ← fetch/push/sync 処理
└──────────┘
登録と更新
if ('serviceWorker' in navigator) {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/',
updateViaCache: 'none'
});
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker?.addEventListener('statechange', () => {
if (newWorker.state === 'activated') {
showToast('新バージョン準備完了、リロードで反映');
}
});
});
}
Cache API:戦略マトリックス
5 つの古典的戦略
| 戦略 | ネットワーク優先 | 適用場面 | オフライン時 |
|---|---|---|---|
| Network Only | ✅ | キャッシュ不可データ(決済/リアルタイム) | 失敗 |
| Network First | ✅+フォールバック | 頻繁更新 API | キャッシュ返却 |
| Cache First | ❌ | 不変リソース(フォント/画像) | キャッシュ返却 |
| Cache Only | ❌ | 事前キャッシュのオフラインページ | キャッシュ返却 |
| Stale While Revalidate | バックグラウンド更新 | HTML/非重要 API | 古いキャッシュ返却 |
戦略セットアップ
const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = [
'/',
'/styles/main.css',
'/scripts/main.js',
'/offline.html',
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(STATIC_ASSETS))
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))
)
)
);
});
Cache First 実装
async function cacheFirst(request: Request): Promise<Response> {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
}
return response;
}
Network First 実装
async function networkFirst(request: Request): Promise<Response> {
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
}
return response;
} catch {
const cached = await caches.match(request);
if (cached) return cached;
return caches.match('/offline.html');
}
}
Stale While Revalidate 実装
async function staleWhileRevalidate(request: Request): Promise<Response> {
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(request);
const fetchPromise = fetch(request).then((response) => {
if (response.ok) cache.put(request, response.clone());
return response;
});
return cached || fetchPromise;
}
ルーティングディスパッチ
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
if (request.method !== 'GET') return;
if (url.pathname.startsWith('/api/')) {
event.respondWith(staleWhileRevalidate(request));
} else if (url.pathname.match(/\.(css|js|woff2)$/)) {
event.respondWith(cacheFirst(request));
} else if (url.pathname.match(/\.(png|jpg|webp|svg)$/)) {
event.respondWith(cacheFirst(request));
} else {
event.respondWith(networkFirst(request));
}
});
Background Sync:バックグラウンド同期
同期タスクの登録
async function registerSync() {
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('submit-pending-data');
}
async function saveOffline(data: object) {
const db = await openDB('outbox', 1, {
upgrade(db) {
db.createObjectStore('pending', { keyPath: 'id', autoIncrement: true });
}
});
await db.add('pending', data);
await registerSync();
}
Service Worker 同期ハンドラ
self.addEventListener('sync', (event) => {
if (event.tag === 'submit-pending-data') {
event.waitUntil(submitPendingData());
}
});
async function submitPendingData() {
const db = await openDB('outbox', 1);
const pending = await db.getAll('pending');
for (const item of pending) {
try {
await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(item),
});
await db.delete('pending', item.id);
} catch {
throw new Error('同期失敗、リトライします');
}
}
}
Push API:プッシュ通知
プッシュ購読
async function subscribePush(publicKey: string) {
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: publicKey
});
await fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify(subscription.toJSON())
});
}
プッシュイベント処理
self.addEventListener('push', (event) => {
const data = event.data?.json() ?? { title: '新メッセージ', body: '' };
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icons/notification.png',
badge: '/icons/badge.png',
vibrate: [200, 100, 200],
data: { url: data.url },
actions: [
{ action: 'open', title: '表示' },
{ action: 'dismiss', title: '却下' }
]
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
const url = event.notification.data?.url || '/';
if (event.action === 'dismiss') return;
event.waitUntil(
self.clients.matchAll({ type: 'window' }).then((clients) => {
for (const client of clients) {
if (client.url === url && 'focus' in client) {
return client.focus();
}
}
return self.clients.openWindow(url);
})
);
});
Workbox:Google 公式ツールチェーン
import { registerRoute } from 'workbox-routing';
import {
CacheFirst,
NetworkFirst,
StaleWhileRevalidate,
} from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [
new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 30 * 24 * 3600 }),
new CacheableResponsePlugin({ statuses: [0, 200] }),
],
})
);
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new StaleWhileRevalidate({
cacheName: 'api',
plugins: [
new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 5 * 60 }),
],
})
);
registerRoute(
({ request }) => request.mode === 'navigate',
new NetworkFirst({
cacheName: 'pages',
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
],
})
);
戦略比較
| 戦略 | 初回ロード | 2回目以降 | オフライン | データ鮮度 | 複雑さ |
|---|---|---|---|---|---|
| Network Only | ネットワーク | ネットワーク | ❌ | 最新 | 低 |
| Network First | ネットワーク | ネット/キャッシュ | ✅ | 比較的新 | 中 |
| Cache First | ネットワーク | キャッシュ | ✅ | 古い可能性 | 低 |
| Stale While Revalidate | ネットワーク | キャッシュ | ✅ | 結果整合 | 中 |
ブラウザ対応状況
| API | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
| Service Worker | 40+ | 44+ | 11.1+ | 17+ |
| Cache API | 40+ | 41+ | 11.1+ | 17+ |
| Background Sync | 49+ | 非対応 | 非対応 | 17+ |
| Push API | 50+ | 44+ | 16+ | 17+ |
| Navigation Preload | 62+ | 非対応 | 非対応 | 17+ |
ベストプラクティス
- 重要リソースの事前キャッシュ:install 時に App Shell をキャッシュしオフライン対応
- バージョン付きキャッシュ名:
app-v1→app-v2、activate で旧キャッシュ削除 - ルーティングディスパッチ:リソースタイプ別に異なる戦略
- キャッシュサイズ制限:ExpirationPlugin で無制限増大を防止
- グレースフルデグラデーション:SW 登録失敗時もページは正常動作
まとめ
Service Worker はオフラインファースト Web アプリ構築の中核です。Cache API の戦略マトリックスでネットワークとキャッシュのインテリジェントなスケジューリングを実現し、Background Sync でオフライン操作の結果整合性を保証、Push API でサーバー主導通知を実現します。Workbox が実装の複雑さを大幅に軽減します。
ファイル圧縮 でキャッシュリソースサイズを削減し、PDF 結合 でオフライン文書を処理、画像圧縮 でキャッシュ画像を最適化できます。
ブラウザローカルツールを無料で試す →
#Service Worker#离线优先#Cache API#后台同步#推送通知