目次
静的サイトジェネレーター(SSG)とService Workerを組み合わせることで、高速でオフライン対応可能なWebアプリケーションを構築できます。しかし、この組み合わせには予想外の落とし穴があります。今回は、実際に遭遇した「デプロイしても古いコンテンツが表示され続ける」問題とその解決方法について共有します。
問題の発生
私のプロジェクトでは、以下のような構成を採用していました:
- SSG(Astro):ビルド時にデータを取得してHTMLを生成
- 自動デプロイ:1日2回、GitHub Actionsでビルドとデプロイを実行
- Service Worker:オフライン対応のためにコンテンツをキャッシュ
- CDN(Cloudflare Pages):コンテンツを配信
一見完璧に見えるこの構成ですが、ユーザーから「最新のデータが表示されない」という報告が寄せられました。
原因の特定
キャッシュレイヤーの調査
問題は複数のキャッシュレイヤーにありました:
- ブラウザのService Workerキャッシュ
- CDNのエッジキャッシュ
- ブラウザの通常のHTTPキャッシュ
特に問題だったのは、Service Workerの実装でした:
// 問題のあるService Worker実装
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// Cache First戦略:キャッシュがあれば常にそれを返す
if (response) {
return response;
}
return fetch(event.request);
})
);
});
この「Cache First」戦略により、一度キャッシュされたHTMLページは、Service Workerを更新しない限り永続的に表示され続けていました。
解決方法
1. キャッシュ戦略の改善
HTMLページには「Network First」戦略を適用しました:
self.addEventListener('fetch', function(event) {
const request = event.request;
const url = new URL(request.url);
// HTMLページは常に最新を取得
if (request.mode === 'navigate' ||
request.headers.get('accept').includes('text/html')) {
event.respondWith(
fetch(request)
.then(function(response) {
// 成功時のみキャッシュを更新
if (response.status === 200) {
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(request, responseToCache);
});
}
return response;
})
.catch(function() {
// ネットワークエラー時のみキャッシュを使用
return caches.match(request);
})
);
} else {
// 静的アセット(CSS、JS、画像)はCache First
event.respondWith(
caches.match(request)
.then(function(response) {
return response || fetch(request);
})
);
}
});
2. キャッシュバージョニング
Service Workerのキャッシュバージョンを管理することで、強制的に古いキャッシュをクリアできるようにしました:
const CACHE_NAME = 'myapp-v1.2'; // バージョンを更新
self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
// 古いバージョンのキャッシュを削除
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});
3. Service Worker の自動更新
Service Workerが確実に更新されるよう、以下のコードを追加しました:
// メインのJavaScriptファイル
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(registration => {
// ページロード時に更新をチェック
registration.update();
// 新しいService Workerが利用可能になったら通知
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' &&
navigator.serviceWorker.controller) {
// 新しいバージョンが利用可能
console.log('新しいバージョンが利用可能です');
window.location.reload();
}
});
});
});
}
キャッシュ戦略のベストプラクティス
リソースタイプごとに適切な戦略を選択することが重要です:
Network First(推奨)
- HTMLページ
- APIレスポンス
- 頻繁に更新されるコンテンツ
Cache First(推奨)
- CSS、JavaScriptファイル
- 画像、フォント
- 変更頻度の低い静的アセット
実装例
const CACHE_STRATEGIES = {
'network-first': [
'/',
'/api/',
'*.html'
],
'cache-first': [
'*.css',
'*.js',
'*.woff2',
'*.png',
'*.jpg'
]
};
function getStrategy(request) {
const url = new URL(request.url);
for (const [strategy, patterns] of Object.entries(CACHE_STRATEGIES)) {
for (const pattern of patterns) {
if (matchPattern(url.pathname, pattern)) {
return strategy;
}
}
}
return 'network-first'; // デフォルト
}
デバッグとトラブルシューティング
Chrome DevToolsの活用
Service Workerの状態を確認する手順:
- DevTools > Application > Service Workers
- 「Update on reload」を有効化(開発時)
- Cache Storage でキャッシュの内容を確認
キャッシュの詳細分析
// コンソールで実行
async function analyzeCaches() {
const cacheNames = await caches.keys();
console.log('キャッシュ一覧:', cacheNames);
for (const name of cacheNames) {
const cache = await caches.open(name);
const requests = await cache.keys();
console.log(`キャッシュ "${name}":`, requests.length, '件');
}
}
analyzeCaches();
まとめ
SSGとService Workerの組み合わせは強力ですが、キャッシュ戦略を慎重に設計する必要があります。
重要なポイント:
- HTMLページは Network First を使用する
- キャッシュバージョニングを実装する
- Service Workerの自動更新を設定する
- 定期的なモニタリングを行う
これらの対策により、ユーザーは常に最新のコンテンツを確実に受け取ることができ、同時にオフライン対応やパフォーマンスの利点も享受できます。
Service Workerは強力な技術ですが、その分複雑さも伴います。適切な実装とモニタリングにより、より良いユーザー体験を提供できるでしょう。