#はじめに

「電波が届かない場所でもWebアプリを使いたい」——Service Workerを使えば、これが可能になります。

Service Workerは、ブラウザとネットワークの間に立つプログラム可能なプロキシです。リクエストをインターセプトし、キャッシュから返したり、ネットワークに転送したりを自由に制御できます。

この記事を読むと、以下のことができるようになります:

  • Service Workerの基本的な仕組みを理解できる
  • Cache APIを使ってキャッシュを操作できる
  • オフライン対応の戦略を選択できる

#Service Workerとは

Service Workerは、Webページとは別のスレッドで動作し、ネットワークリクエストをインターセプトできるJavaScriptファイルです。

[Webページ] ←→ [Service Worker] ←→ [ネットワーク / キャッシュ]

#特徴

  • バックグラウンド動作: ページが閉じていても動作可能
  • プログラム可能: キャッシュ戦略を自由に実装
  • HTTPS必須: セキュリティ上、HTTPSでのみ動作(localhost除く)
  • 非同期: すべての処理がPromiseベース

#Service Workerのライフサイクル

#1. 登録(Registration)

メインページからService Workerを登録します。

// main.js
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(registration => {
      console.log('SW registered:', registration.scope);
    })
    .catch(error => {
      console.error('SW registration failed:', error);
    });
}

#2. インストール(Install)

初回登録時、または更新時に実行されます。

// sw.js
self.addEventListener('install', event => {
  console.log('SW installing...');

  // 事前キャッシュ
  event.waitUntil(
    caches.open('v1').then(cache => {
      return cache.addAll([
        '/',
        '/index.html',
        '/style.css',
        '/app.js',
      ]);
    })
  );
});

#3. 有効化(Activate)

インストール後、古いService Workerが制御していたページがすべて閉じられると有効化されます。

self.addEventListener('activate', event => {
  console.log('SW activated');

  // 古いキャッシュの削除
  event.waitUntil(
    caches.keys().then(keys => {
      return Promise.all(
        keys.filter(key => key !== 'v1')
            .map(key => caches.delete(key))
      );
    })
  );
});

#4. フェッチ(Fetch)

ページからのリクエストをインターセプトします。

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      return response || fetch(event.request);
    })
  );
});

#Cache API

Service Worker内でキャッシュを操作するためのAPIです。

#キャッシュを開く

const cache = await caches.open('my-cache-v1');

#リクエストをキャッシュに追加

// 単一のリクエスト
await cache.add('/style.css');

// 複数のリクエスト
await cache.addAll(['/index.html', '/app.js', '/style.css']);

#カスタムレスポンスをキャッシュ

const response = await fetch('/data.json');
await cache.put('/data.json', response);

#キャッシュからの取得

const response = await cache.match('/style.css');
if (response) {
  // キャッシュヒット
} else {
  // キャッシュミス
}

#キャッシュの削除

await cache.delete('/old-file.js');
await caches.delete('old-cache-v1');

#キャッシュ戦略

#1. Cache First(キャッシュ優先)

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(cached => {
      return cached || fetch(event.request);
    })
  );
});
  • まずキャッシュを確認
  • なければネットワークから取得
  • 適用場面: 静的アセット、変更頻度の低いリソース

#2. Network First(ネットワーク優先)

self.addEventListener('fetch', event => {
  event.respondWith(
    fetch(event.request)
      .catch(() => caches.match(event.request))
  );
});
  • まずネットワークから取得を試みる
  • 失敗したらキャッシュを使用
  • 適用場面: 最新データが重要なAPI、ニュース

#3. Stale-While-Revalidate

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(cached => {
      const fetchPromise = fetch(event.request).then(response => {
        caches.open('dynamic').then(cache => {
          cache.put(event.request, response.clone());
        });
        return response;
      });
      return cached || fetchPromise;
    })
  );
});
  • キャッシュがあれば即座に返す
  • 同時にネットワークから取得してキャッシュを更新
  • 適用場面: アバター画像、ソーシャルフィード

#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));
});
  • 常にネットワークから取得
  • 適用場面: 分析リクエスト、非GETリクエスト

#戦略の使い分け

self.addEventListener('fetch', event => {
  const url = new URL(event.request.url);

  if (url.pathname.startsWith('/api/')) {
    // API: Network First
    event.respondWith(networkFirst(event.request));
  } else if (url.pathname.match(/\.[a-f0-9]{8}\./)) {
    // ハッシュ付きアセット: Cache First
    event.respondWith(cacheFirst(event.request));
  } else if (url.pathname.endsWith('.html')) {
    // HTML: Network First + Offline Fallback
    event.respondWith(networkFirstWithFallback(event.request));
  }
});

#Service Workerの更新

#更新の検出

ブラウザは登録されたService Workerファイルを定期的にチェックし、バイト単位で異なれば新しいバージョンをインストールします。

#更新フロー

1. 新しいsw.jsがダウンロードされる
2. installイベントが発火(新バージョン)
3. 古いSWがまだアクティブ
4. すべてのタブが閉じられる
5. activateイベントが発火(新バージョン)
6. 新しいSWがアクティブに

#即座に有効化

self.addEventListener('install', event => {
  self.skipWaiting();  // 待機をスキップ
});

self.addEventListener('activate', event => {
  event.waitUntil(clients.claim());  // 既存のクライアントを制御
});

注意: 破壊的な変更がある場合、古いページと新しいService Workerの不整合が起きる可能性があります。

#DevToolsでの確認

#Application > Service Workers

  • 登録状態
  • ライフサイクル状態
  • 更新、停止、削除

#Application > Cache Storage

  • キャッシュ名の一覧
  • キャッシュ内のリソース
  • 手動での削除

#オフラインテスト

  1. Network タブで「Offline」をチェック
  2. ページをリロード
  3. オフライン対応が機能しているか確認

#Workbox

Workboxは、Service Workerのキャッシュ戦略を簡単に実装するためのライブラリです。

#使用例

import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';

// 画像: Cache First
registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({ cacheName: 'images' })
);

// API: Network First
registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new NetworkFirst({ cacheName: 'api' })
);

// CSS/JS: Stale While Revalidate
registerRoute(
  ({ request }) => request.destination === 'style' || request.destination === 'script',
  new StaleWhileRevalidate({ cacheName: 'static' })
);

ビルドツール連携で事前キャッシュも自動化できます。

#まとめ

  • Service Workerはネットワークリクエストをインターセプトするプロキシ
  • Cache APIでプログラム的にキャッシュを制御
  • 主な戦略: Cache First、Network First、Stale-While-Revalidate
  • 用途に応じて戦略を使い分ける
  • Workboxを使うと実装が簡単に
  • 更新戦略を慎重に設計する

#次のステップ

キャッシュ戦略モジュールはこれで完了です。HTTPキャッシュの基本、Cache-Controlディレクティブ、条件付きリクエスト、CDNキャッシュ、キャッシュバスティング、そしてService Workerについて学びました。

次のモジュールでは、パフォーマンスと配信について学びます。DNS、CDN、圧縮、画像最適化、Core Web Vitalsなど、Webサイトを高速化するための技術を紹介します。

#参考リンク