Service Worker 応用:オフラインファーストアーキテクチャとバックグラウンド同期戦略

技术架构(更新: 2026年6月4日)

Service Worker:オフラインファーストアーキテクチャの中核

Service Worker はブラウザ内でページから独立したバックグラウンドスレッドで、ネットワークリクエストの傍受、キャッシュ管理、プッシュ処理が可能です:

特性 説明
実行環境 独立 Worker スレッド、DOM アクセスなし
ライフサイクル install → activate → fetch
スコープ 登録パスとそのサブパス
通信 postMessage / MessageChannel
永続化 イベント駆動、メモリ常駐なし

ライフサイクル詳解

                  ┌──────────┐
                  │  parsed  │
                  └────┬─────┘
                       │ install()
                  ┌────▼─────┐
            ┌─────│installing│
            │     └────┬─────┘
            │          │ waitUntil()
            │     ┌────▼─────┐
            │     │ installed │ (waiting)
            │     └────┬─────┘
            │          │ activate()
            │     ┌────▼─────┐
            └────▶│activating│
                  └────┬─────┘
                       │
                  ┌────▼─────┐
                  │ activated │ ← fetch/push/sync 処理
                  └──────────┘

登録と更新

if ('serviceWorker' in navigator) {
  const registration = await navigator.serviceWorker.register('/sw.js', {
    scope: '/',
    updateViaCache: 'none'
  });

  registration.addEventListener('updatefound', () => {
    const newWorker = registration.installing;
    newWorker?.addEventListener('statechange', () => {
      if (newWorker.state === 'activated') {
        showToast('新バージョン準備完了、リロードで反映');
      }
    });
  });
}

Cache API:戦略マトリックス

5 つの古典的戦略

戦略 ネットワーク優先 適用場面 オフライン時
Network Only キャッシュ不可データ(決済/リアルタイム) 失敗
Network First ✅+フォールバック 頻繁更新 API キャッシュ返却
Cache First 不変リソース(フォント/画像) キャッシュ返却
Cache Only 事前キャッシュのオフラインページ キャッシュ返却
Stale While Revalidate バックグラウンド更新 HTML/非重要 API 古いキャッシュ返却

戦略セットアップ

const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = [
  '/',
  '/styles/main.css',
  '/scripts/main.js',
  '/offline.html',
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => cache.addAll(STATIC_ASSETS))
  );
});

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(
        keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))
      )
    )
  );
});

Cache First 実装

async function cacheFirst(request: Request): Promise<Response> {
  const cached = await caches.match(request);
  if (cached) return cached;

  const response = await fetch(request);
  if (response.ok) {
    const cache = await caches.open(CACHE_NAME);
    cache.put(request, response.clone());
  }
  return response;
}

Network First 実装

async function networkFirst(request: Request): Promise<Response> {
  try {
    const response = await fetch(request);
    if (response.ok) {
      const cache = await caches.open(CACHE_NAME);
      cache.put(request, response.clone());
    }
    return response;
  } catch {
    const cached = await caches.match(request);
    if (cached) return cached;
    return caches.match('/offline.html');
  }
}

Stale While Revalidate 実装

async function staleWhileRevalidate(request: Request): Promise<Response> {
  const cache = await caches.open(CACHE_NAME);
  const cached = await cache.match(request);

  const fetchPromise = fetch(request).then((response) => {
    if (response.ok) cache.put(request, response.clone());
    return response;
  });

  return cached || fetchPromise;
}

ルーティングディスパッチ

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

  if (request.method !== 'GET') return;

  if (url.pathname.startsWith('/api/')) {
    event.respondWith(staleWhileRevalidate(request));
  } else if (url.pathname.match(/\.(css|js|woff2)$/)) {
    event.respondWith(cacheFirst(request));
  } else if (url.pathname.match(/\.(png|jpg|webp|svg)$/)) {
    event.respondWith(cacheFirst(request));
  } else {
    event.respondWith(networkFirst(request));
  }
});

Background Sync:バックグラウンド同期

同期タスクの登録

async function registerSync() {
  const registration = await navigator.serviceWorker.ready;
  await registration.sync.register('submit-pending-data');
}

async function saveOffline(data: object) {
  const db = await openDB('outbox', 1, {
    upgrade(db) {
      db.createObjectStore('pending', { keyPath: 'id', autoIncrement: true });
    }
  });
  await db.add('pending', data);
  await registerSync();
}

Service Worker 同期ハンドラ

self.addEventListener('sync', (event) => {
  if (event.tag === 'submit-pending-data') {
    event.waitUntil(submitPendingData());
  }
});

async function submitPendingData() {
  const db = await openDB('outbox', 1);
  const pending = await db.getAll('pending');

  for (const item of pending) {
    try {
      await fetch('/api/submit', {
        method: 'POST',
        body: JSON.stringify(item),
      });
      await db.delete('pending', item.id);
    } catch {
      throw new Error('同期失敗、リトライします');
    }
  }
}

Push API:プッシュ通知

プッシュ購読

async function subscribePush(publicKey: string) {
  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: publicKey
  });
  await fetch('/api/push/subscribe', {
    method: 'POST',
    body: JSON.stringify(subscription.toJSON())
  });
}

プッシュイベント処理

self.addEventListener('push', (event) => {
  const data = event.data?.json() ?? { title: '新メッセージ', body: '' };

  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icons/notification.png',
      badge: '/icons/badge.png',
      vibrate: [200, 100, 200],
      data: { url: data.url },
      actions: [
        { action: 'open', title: '表示' },
        { action: 'dismiss', title: '却下' }
      ]
    })
  );
});

self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  const url = event.notification.data?.url || '/';

  if (event.action === 'dismiss') return;

  event.waitUntil(
    self.clients.matchAll({ type: 'window' }).then((clients) => {
      for (const client of clients) {
        if (client.url === url && 'focus' in client) {
          return client.focus();
        }
      }
      return self.clients.openWindow(url);
    })
  );
});

Workbox:Google 公式ツールチェーン

import { registerRoute } from 'workbox-routing';
import {
  CacheFirst,
  NetworkFirst,
  StaleWhileRevalidate,
} from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';

registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images',
    plugins: [
      new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 30 * 24 * 3600 }),
      new CacheableResponsePlugin({ statuses: [0, 200] }),
    ],
  })
);

registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new StaleWhileRevalidate({
    cacheName: 'api',
    plugins: [
      new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 5 * 60 }),
    ],
  })
);

registerRoute(
  ({ request }) => request.mode === 'navigate',
  new NetworkFirst({
    cacheName: 'pages',
    plugins: [
      new CacheableResponsePlugin({ statuses: [0, 200] }),
    ],
  })
);

戦略比較

戦略 初回ロード 2回目以降 オフライン データ鮮度 複雑さ
Network Only ネットワーク ネットワーク 最新
Network First ネットワーク ネット/キャッシュ 比較的新
Cache First ネットワーク キャッシュ 古い可能性
Stale While Revalidate ネットワーク キャッシュ 結果整合

ブラウザ対応状況

API Chrome Firefox Safari Edge
Service Worker 40+ 44+ 11.1+ 17+
Cache API 40+ 41+ 11.1+ 17+
Background Sync 49+ 非対応 非対応 17+
Push API 50+ 44+ 16+ 17+
Navigation Preload 62+ 非対応 非対応 17+

ベストプラクティス

  1. 重要リソースの事前キャッシュ:install 時に App Shell をキャッシュしオフライン対応
  2. バージョン付きキャッシュ名app-v1app-v2、activate で旧キャッシュ削除
  3. ルーティングディスパッチ:リソースタイプ別に異なる戦略
  4. キャッシュサイズ制限:ExpirationPlugin で無制限増大を防止
  5. グレースフルデグラデーション:SW 登録失敗時もページは正常動作

まとめ

Service Worker はオフラインファースト Web アプリ構築の中核です。Cache API の戦略マトリックスでネットワークとキャッシュのインテリジェントなスケジューリングを実現し、Background Sync でオフライン操作の結果整合性を保証、Push API でサーバー主導通知を実現します。Workbox が実装の複雑さを大幅に軽減します。

ファイル圧縮 でキャッシュリソースサイズを削減し、PDF 結合 でオフライン文書を処理、画像圧縮 でキャッシュ画像を最適化できます。

ブラウザローカルツールを無料で試す →

#Service Worker#离线优先#Cache API#后台同步#推送通知