ブラウザの枠を超えて:Service Worker で堅牢な PWA を構築する

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

ウェブとモバイルの間の苛立たしい隔たり

電車の中で記事を読んでいるときに電波が数秒間途切れ、突然「インターネットに接続されていません」という恐竜の画面が表示される。誰もが経験したことがあるはずです。長年、これがウェブアプリの致命的な弱点でした。ネイティブアプリは通信が途切れても機能し続けますが、ウェブアプリはサーバーからの絶え間ない応答がなければ、ただ停止してしまいます。

この問題の原因は、標準的なブラウザのリクエストモデルにあります。デフォルトでは、ナビゲーションやリフレッシュのたびに、ブラウザはネットワークから直接リソースを取得しようとします。接続に失敗すれば、リクエストも失敗します。これを解決するには、ブラウザとネットワークの間に介在するプロキシが必要です。それこそが、Service Worker の役割です。

クイックスタート:5分でサイトを進化させる

標準的なサイトを Progressive Web App (PWA) に変換するには、manifest.json ファイルと登録済みの Service Worker という2つの主要なコンポーネントが必要です。私の経験では、これら基本を実装するだけで、UI が瞬時に読み込まれるようになり、不安定な 3G ネットワークにおける直帰率を最大 20% 削減できます。

1. ウェブアプリマニフェストの作成

マニフェストは、インストールされた際のアプリアクションを OS に伝えます。ブラウザの URL バーを非表示にし、ブランドカラーを設定できます。ルートディレクトリに manifest.json を作成してください:

{
  "short_name": "FastApp",
  "name": "マイ高性能 PWA",
  "icons": [
    {
      "src": "/images/icon-192.png",
      "type": "image/png",
      "sizes": "192x192"
    },
    {
      "src": "/images/icon-512.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ],
  "start_url": "/",
  "background_color": "#ffffff",
  "display": "standalone",
  "theme_color": "#2f55d4"
}

2. Service Worker の登録

メインの JavaScript ファイルに以下の登録ロジックを追加します。このスクリプトはエントリポイントとして機能し、Service Worker ファイルの場所をブラウザに伝えます。

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then(reg => console.log('Service Worker が有効になりました!', reg.scope))
      .catch(err => console.error('登録に失敗しました:', err));
  });
}

3. 基本的な sw.js の作成

ルートに空の sw.js ファイルを作成することから始めます。マニフェストがあり HTTPS が有効であれば、空のファイルであっても Chrome の「アプリをインストール」プロンプトを表示する基準を満たせます。

Service Worker のライフサイクルをマスターする

Service Worker は特殊な Web Worker です。バックグラウンドで動作し、メインスレッドとは独立して動き、DOM を操作することはできません。通常のページライフサイクルの外側に存在するため、標準的なスクリプトとは異なる挙動を示します。

インストールフェーズ

install イベントは、ブラウザが初めてスクリプトを発見したとき、またはファイル内のバイト単位の変更を検知したときに発生します。これは「App Shell」(ネットワークなしでインターフェースをレンダリングするために必要な HTML、CSS、コア JS)をキャッシュする絶好のタイミングです。

const CACHE_NAME = 'v1_static_assets';
const ASSETS = [
  '/',
  '/index.html',
  '/css/style.css',
  '/js/app.js',
  '/images/logo.png'
];

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => {
      // アセットをキャッシュに追加
      return cache.addAll(ASSETS);
    })
  );
});

アクティベーションフェーズ

アクティベーションは、古い Service Worker が解放され、新しい Service Worker が制御を開始したときに発生します。この段階を使用して、古いキャッシュを削除します。これにより、ユーザーのデバイスが古い CSS や古いバージョンのファイルで一杯になるのを防ぎます。

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

Fetch イベント:リクエストのインターセプト

ここが PWA の心臓部です。アプリが行うすべてのネットワークリクエストはこのイベントを通過します。キャッシュからファイルを提供するか、ウェブから取得するか、あるいはその場でレスポンスを生成するかを選択できます。

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      // キャッシュがあればそれを返し、なければネットワークへリクエスト
      return response || fetch(event.request);
    })
  );
});

適切なキャッシュ戦略の選択

すべてのデータが同じではありません。企業のロゴはめったに変わりませんが、株価チャートは毎秒更新されます。間違った戦略を使用すると、データが古くなったり、不要なローディング画面が表示されたりします。

Cache First(静的アセットに最適)

フォント、アイコン、スタイルシートに使用します。アプリはまずキャッシュを確認します。ファイルがあれば数ミリ秒で読み込まれます。キャッシュが空の場合のみネットワークにアクセスします。

Network First(動的データに最適)

API 呼び出しやニュースフィードに最適です。アプリはまず最新のデータを得るためにネットワークを試行します。ユーザーがトンネル内にいたり、Wi-Fi が不安定だったりする場合は、最後にキャッシュされたバージョンにフォールバックします。

self.addEventListener('fetch', event => {
  if (event.request.url.includes('/api/')) {
    event.respondWith(
      fetch(event.request)
        .then(response => {
          const resClone = response.clone();
          caches.open('dynamic-data').then(cache => cache.put(event.request, resClone));
          return response;
        })
        .catch(() => caches.match(event.request)) // オフライン時はキャッシュから取得
    );
  }
});

Stale-While-Revalidate

これは本番環境における「黄金律」です。キャッシュされたバージョンを即座に提供するため、ユーザーはすぐにコンテンツを見ることができます。その間にバックグラウンドで更新を取得し、次回のアクセス時に差し替えます。

本番環境に向けた実践的なアドバイス

PWA の構築はシンプルですが、堅牢なものにするには細部への注意が必要です。実際のデプロイから学んだ 4 つの教訓を紹介します:

  • HTTPS は必須条件: Service Worker はすべてのネットワークトラフィックをインターセプトできます。この強力な権限のため、ブラウザは安全な HTTPS 接続を要求します(テスト中の localhost を除く)。
  • 更新の管理は慎重に: ブラウザは 24 時間ごとに sw.js の更新をチェックします。新しいバージョンが存在する場合、古いバージョンを使用しているすべてのタブが閉じられるまで待機します。self.skipWaiting() を使用して強制的に更新することもできますが、ユーザーの現在のセッションを壊さないよう注意が必要です。
  • DevTools の活用: Chrome DevTools の Application タブを活用しましょう。オフラインモードのシミュレート、Worker の強制更新、個別のキャッシュバケットの検査が可能です。
  • Lighthouse スコア 100 を目指す: Chrome で Lighthouse 監査を実行しましょう。マニフェストの設定、有効なアイコン、3G スピードでもインタラクティブな状態を維持できているかを確認します。

Service Worker を実装することで、アーキテクチャが脆弱な「常時オンライン」モデルから「オフラインファースト」の考え方へとシフトします network を必須要件ではなく「オプションの拡張機能」として扱うことで、ユーザーの場所に関係なく、高速で信頼性の高い体験を提供できるようになります。

Share: