Service WorkerとSSGの組み合わせで起きるキャッシュ問題の解決法

公開日:
目次

静的サイトジェネレーター(SSG)とService Workerを組み合わせることで、高速でオフライン対応可能なWebアプリケーションを構築できます。しかし、この組み合わせには予想外の落とし穴があります。今回は、実際に遭遇した「デプロイしても古いコンテンツが表示され続ける」問題とその解決方法について共有します。

問題の発生

私のプロジェクトでは、以下のような構成を採用していました:

  • SSG(Astro):ビルド時にデータを取得してHTMLを生成
  • 自動デプロイ:1日2回、GitHub Actionsでビルドとデプロイを実行
  • Service Worker:オフライン対応のためにコンテンツをキャッシュ
  • CDN(Cloudflare Pages):コンテンツを配信

一見完璧に見えるこの構成ですが、ユーザーから「最新のデータが表示されない」という報告が寄せられました。

原因の特定

キャッシュレイヤーの調査

問題は複数のキャッシュレイヤーにありました:

  1. ブラウザのService Workerキャッシュ
  2. CDNのエッジキャッシュ
  3. ブラウザの通常の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の状態を確認する手順:

  1. DevTools > Application > Service Workers
  2. 「Update on reload」を有効化(開発時)
  3. 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の組み合わせは強力ですが、キャッシュ戦略を慎重に設計する必要があります。

重要なポイント:

  1. HTMLページは Network First を使用する
  2. キャッシュバージョニングを実装する
  3. Service Workerの自動更新を設定する
  4. 定期的なモニタリングを行う

これらの対策により、ユーザーは常に最新のコンテンツを確実に受け取ることができ、同時にオフライン対応やパフォーマンスの利点も享受できます。

Service Workerは強力な技術ですが、その分複雑さも伴います。適切な実装とモニタリングにより、より良いユーザー体験を提供できるでしょう。