Service Workerのキャッシュ戦略改善:デプロイ後の即時反映を実現
問題の背景
当ブログでは、パフォーマンス向上のためにService Workerを実装していましたが、以下の問題に直面しました:
- 新しい記事をデプロイしても、古い記事が表示され続ける
- 強制リロード(Ctrl+Shift+R)しないと最新コンテンツが表示されない
- ユーザーが新しいコンテンツの存在に気づかない
この問題は、Service WorkerがCache First戦略を全てのリソースに適用していたことが原因でした。
Service Workerとは
Service Workerは、ブラウザがバックグラウンドで実行するJavaScriptで、以下の特徴があります:
- Webページとネットワークの間でプロキシサーバーとして動作
- オフライン対応やキャッシュ制御を実現
- Progressive Web App (PWA)の中核技術
改善前の問題点
旧実装のキャッシュ戦略
// 全てのリクエストに対してCache Firstを適用
event.respondWith(
caches.match(event.request).then(response => {
if (response) {
// キャッシュがあれば常にそれを返す(問題の原因)
return response;
}
// キャッシュがない場合のみネットワークから取得
return fetch(event.request);
})
);
この実装では、一度キャッシュされたHTMLは、キャッシュが削除されるまで更新されません。
実施した改善
1. ハイブリッドキャッシュ戦略の実装
コンテンツタイプに応じて異なるキャッシュ戦略を適用:
HTMLドキュメント:Network First戦略
// HTMLドキュメントの判定
const isHTMLDocument = event.request.destination === 'document' ||
event.request.headers.get('accept')?.includes('text/html') ||
url.endsWith('/') ||
url.includes('/posts/') ||
url.includes('/tags/');
if (isHTMLDocument) {
event.respondWith(
// まずネットワークから取得を試みる
fetch(event.request)
.then(response => {
// 成功したらキャッシュを更新
const responseToCache = response.clone();
caches.open(DYNAMIC_CACHE).then(cache => {
cache.put(event.request, responseToCache);
});
return response;
})
.catch(() => {
// オフライン時はキャッシュから配信
return caches.match(event.request);
})
);
}
静的アセット:Cache First + Background Update
// CSS/JSファイルはキャッシュ優先 + バックグラウンド更新
if (response) {
// キャッシュから即座に返す
if (url.includes('/css/') || url.includes('/js/')) {
// バックグラウンドで最新版を取得
fetch(event.request).then(freshResponse => {
if (freshResponse && freshResponse.status === 200) {
caches.open(cacheName).then(cache => {
cache.put(event.request, freshResponse.clone());
});
}
});
}
return response;
}
2. キャッシュバージョニングの強化
// バージョン管理を明確化
const CACHE_VERSION = 'v3.0.0';
const CACHE_NAME = `karuta-${CACHE_VERSION}`;
const STATIC_CACHE = `karuta-static-${CACHE_VERSION}`;
const DYNAMIC_CACHE = `karuta-dynamic-${CACHE_VERSION}`;
3. 更新通知UIの改善
ユーザーフレンドリーな更新通知を実装:
function showUpdateNotification() {
const notification = document.createElement('div');
notification.className = 'sw-update-notification';
notification.innerHTML = `
<div class="update-notification-content">
<div class="update-notification-message">
<strong>🔄 新しいコンテンツが利用可能です</strong>
<p>最新の記事やコンテンツを表示するには、ページを更新してください。</p>
</div>
<div class="update-notification-actions">
<button onclick="window.location.reload()">今すぐ更新</button>
<button onclick="this.closest('.sw-update-notification').remove()">後で</button>
</div>
</div>
`;
document.body.appendChild(notification);
}
4. 定期的な更新チェック
// 1時間ごとにService Workerの更新をチェック
setInterval(() => {
registration.update();
}, 60 * 60 * 1000);
改善効果
ユーザー体験の向上
- 即時性: 新しい記事が公開されたら、次回アクセス時に自動的に最新版を表示
- オフライン対応: ネットワークが不安定でもキャッシュから高速表示
- 透明性: 更新がある場合は分かりやすい通知を表示
パフォーマンスの維持
- 静的アセット(CSS/JS/画像)は引き続きキャッシュ優先で高速表示
- バックグラウンド更新により、パフォーマンスを犠牲にせずに最新性を確保
技術的なポイント
1. リクエストの判定
// 複数の条件でHTMLドキュメントを判定
const isHTMLDocument =
event.request.destination === 'document' || // 標準的な判定
event.request.headers.get('accept')?.includes('text/html') || // Acceptヘッダー
url.endsWith('/') || // ディレクトリアクセス
url.includes('/posts/'); // 特定パスパターン
2. レスポンスのクローン
// Streamは一度しか読めないため、クローンが必要
const responseToCache = response.clone();
3. ダークモード対応
.theme-dark .sw-update-notification {
background: #2d2d2d;
border-color: #4a9eff;
}
まとめ
Service Workerのキャッシュ戦略を以下のように改善しました:
| コンテンツタイプ | 旧戦略 | 新戦略 |
|---|---|---|
| HTMLドキュメント | Cache First | Network First |
| CSS/JSファイル | Cache First | Cache First + Background Update |
| 画像ファイル | Cache First | Cache First |
これにより、最新性とパフォーマンスのバランスを実現し、ユーザーは強制リロードなしで常に最新のコンテンツを閲覧できるようになりました。
今後の展望
- Push APIを活用した新着記事の通知
- Workboxライブラリの導入検討
- キャッシュサイズの最適化
- 差分更新の実装
Service Workerは強力な技術ですが、適切なキャッシュ戦略の設計が重要です。今回の改善により、ユーザー体験を大幅に向上させることができました。

