Service Worker で作る PWA 完全ガイド|オフライン対応とキャッシュ戦略【2026年版】
Service Worker の基礎から実装まで完全解説。キャッシュ戦略、Workbox活用、インストール可能なPWAの作り方を実践的に学べる総合ガイド。
Progressive Web App(PWA)の中核技術である Service Worker は、オフライン対応、プッシュ通知、バックグラウンド同期など、ネイティブアプリに近い体験を Web で実現するための強力な API です。2026年現在、すべての主要ブラウザでサポートされ、実務投入が標準化しています。
Service Worker とは
Service Worker は、Web ページとは別スレッドで動作する JavaScript です。ブラウザとネットワークの間に入り、リクエストを傍受・操作できます。
主な用途:
- オフライン対応: キャッシュから表示
- 高速化: 二度目以降のアクセスを高速化
- プッシュ通知: バックグラウンドでの通知受信
- バックグラウンド同期: オフライン時の操作を復帰時に送信
Service Worker の基本
登録
// main.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('SW registered:', reg))
.catch(err => console.error('SW registration failed:', err));
});
}
Service Worker ファイル
// sw.js
const CACHE_NAME = 'my-site-v1';
const ASSETS = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/logo.svg',
];
// インストール時: アセットをキャッシュ
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS))
);
});
// アクティベート時: 古いキャッシュを削除
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(
keys.filter(key => key !== CACHE_NAME)
.map(key => caches.delete(key))
)
)
);
});
// フェッチ時: キャッシュから返す
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(res => res || fetch(event.request))
);
});
キャッシュ戦略の5パターン
1. Cache First(キャッシュ優先)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(cached => {
return cached || fetch(event.request);
})
);
});
用途: 画像、CSS、フォント等の静的アセット メリット: 高速、オフライン対応 デメリット: 更新が反映されにくい
2. Network First(ネットワーク優先)
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then(res => {
const clone = res.clone();
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
return res;
})
.catch(() => caches.match(event.request))
);
});
用途: HTML、API レスポンス等、常に最新が欲しいもの メリット: 常に最新データ デメリット: ネットワーク遅い時は体感が悪い
3. Stale While Revalidate(古くても返しつつ更新)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then(cached => {
const fetchPromise = fetch(event.request).then(res => {
caches.open(CACHE_NAME).then(cache => cache.put(event.request, res.clone()));
return res;
});
return cached || fetchPromise;
})
);
});
用途: ブログ記事、商品一覧等 メリット: 高速 + 最終的に最新化 デメリット: 1回目は古いデータが返る可能性
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));
});
用途: 分析データの送信、常に最新でなければならない API
Workbox でキャッシュを効率管理
Workbox は Google 公式の Service Worker ライブラリで、キャッシュ戦略を宣言的に書けます。
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
import { ExpirationPlugin } from 'workbox-expiration';
// ビルド時に生成される manifest を使ってプリキャッシュ
precacheAndRoute(self.__WB_MANIFEST);
// 画像は Cache First(30日保持)
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [
new CacheableResponsePlugin({ statuses: [0, 200] }),
new ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60,
}),
],
})
);
// HTML は Network First
registerRoute(
({ request }) => request.destination === 'document',
new NetworkFirst({
cacheName: 'pages',
networkTimeoutSeconds: 3,
})
);
// CSS/JS は Stale While Revalidate
registerRoute(
({ request }) => ['style', 'script'].includes(request.destination),
new StaleWhileRevalidate({ cacheName: 'assets' })
);
Web App Manifest でインストール可能に
PWA としてホーム画面に追加できるようにするには、Web App Manifest が必要です。
{
"name": "Code & Craft",
"short_name": "C&C",
"description": "Web制作の実践ノウハウを発信するテックブログ",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#0284c7",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
HTML で読み込み:
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#0284c7" />
これで条件を満たしたユーザーに「インストール」のプロンプトが表示されます。
プッシュ通知
Service Worker を使って、サイトを閉じていても通知を送れます。
// sw.js
self.addEventListener('push', (event) => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png',
data: { url: data.url },
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
});
バックグラウンド同期
オフライン時の操作を記録し、オンライン復帰時に送信します。
// main.js
navigator.serviceWorker.ready.then(reg => {
reg.sync.register('send-message');
});
// sw.js
self.addEventListener('sync', (event) => {
if (event.tag === 'send-message') {
event.waitUntil(sendQueuedMessages());
}
});
デバッグとトラブルシューティング
Chrome DevTools
Application タブ → Service Workers で以下を確認できます。
- 登録状況
- ライフサイクル状態
- ログ確認
- 手動でのアップデート
よくあるトラブル
1. Service Worker が更新されない
- HTTPS 環境でのみ動作(localhost は例外)
- ブラウザの強制更新(Ctrl+Shift+R)
self.skipWaiting()とclients.claim()を使う
self.addEventListener('install', () => {
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(clients.claim());
});
2. キャッシュが肥大化する
ExpirationPluginで最大サイズと有効期限を設定- バージョン管理で古いキャッシュを削除
3. オフライン時にエラーページを表示したい
self.addEventListener('fetch', (event) => {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(() => caches.match('/offline.html'))
);
}
});
パフォーマンスへの影響
適切に実装された Service Worker は、2回目以降のページロードを50-90%高速化できます。Core Web Vitals の LCP 改善にも大きく寄与します。
まとめ
Service Worker と PWA 技術により、Web サイトは以下を実現できます。
- ネイティブアプリ並みの体験: オフライン対応、ホーム画面追加
- 圧倒的な高速化: キャッシュ戦略による応答速度向上
- エンゲージメント向上: プッシュ通知で再訪促進
まずは Workbox を使って、画像と静的アセットのキャッシュから始めるのがおすすめです。段階的に対象を広げていけば、完全な PWA に進化させられます。