メインコンテンツにスキップ

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);

改善効果

ユーザー体験の向上

  1. 即時性: 新しい記事が公開されたら、次回アクセス時に自動的に最新版を表示
  2. オフライン対応: ネットワークが不安定でもキャッシュから高速表示
  3. 透明性: 更新がある場合は分かりやすい通知を表示

パフォーマンスの維持

  • 静的アセット(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 FirstNetwork First
CSS/JSファイルCache FirstCache First + Background Update
画像ファイルCache FirstCache First

これにより、最新性とパフォーマンスのバランスを実現し、ユーザーは強制リロードなしで常に最新のコンテンツを閲覧できるようになりました。

今後の展望

  • Push APIを活用した新着記事の通知
  • Workboxライブラリの導入検討
  • キャッシュサイズの最適化
  • 差分更新の実装

Service Workerは強力な技術ですが、適切なキャッシュ戦略の設計が重要です。今回の改善により、ユーザー体験を大幅に向上させることができました。

参考リンク