PWAで画面遷移できない原因は?Service Workerとキャッシュの確認方法

PWA / Service Worker / キャッシュ / 個人開発


PWAでリンクやボタンを押しても、次のページが開かない。そんなときは、Service Workerを経由した場合だけ失敗するのかを先に確かめると、調べる場所を絞れます。

私の飲み会カウンター「何杯飲んだっけ?」でも、記録を保存した後に履歴画面へ進めなくなりました。履歴ページのHTMLはサーバーにあり、HTTPで取得すれば200が返ります。それでも、Service Workerが有効なChromeでは ERR_FAILED になっていました。

この記事では、PCのChrome・Edgeの開発者ツールで確認する順番と、このアプリで見つかった原因をまとめます。

まず、どの操作で止まるかを確認する

「画面遷移できない」には、リンク先の間違い、JavaScriptのエラー、Service Workerが返すレスポンスの問題などが含まれます。最初に、失敗する操作とURLを確認します。

確認すること次に調べる場所
ボタンを押しても、遷移先への要求が出ないクリック処理とConsoleのエラー
遷移先が404になるリンクの相対パスと公開ファイルの配置
SWを迂回したときだけ開けるSWのfetch処理とCache Storage
ページは開くが、別ウィンドウやアドレスバーが出る遷移先URLとmanifestのscope

「何杯飲んだっけ?」では、保存後に次の処理で履歴へ移動していました。

location.href = 'history.html';

本番の配置は /drink-session-counter/ 配下なので、移動先は /drink-session-counter/history.html です。ここで、開発環境のルート / に置いていたときと同じつもりで /history.html と書くと、サイト直下を開いてしまいます。

リンクの文字列だけでなく、実際にどのURLへ移動しようとしているかを見ます。

Service Workerを迂回して同じ操作を試す

Chrome・Edgeでは、開発者ツールからService Workerを一時的に迂回できます。

  1. 対象のページで F12 を押し、開発者ツールを開く
  2. ApplicationService workers を開く
  3. Bypass for network にチェックを入れる
  4. ページを再読み込みして、失敗したリンクやボタンをもう一度押す
  5. 比較が終わったらチェックを外す

この方法はMicrosoftのPWAデバッグ資料にも載っています。

迂回すると開けるなら、SWやSWから返されるキャッシュが原因候補になります。変わらなければ、URLやページ側のJavaScriptも引き続き調べます。

最初からサイトのデータを全部削除する必要はありません。 Clear site data などで保存領域まで消すと、localStorage やIndexedDBに置いたアプリの記録も失うことがあります。このカウンターも記録を端末内に保存しているので、まずデータを残したまま比較する方法が合っています。

HTTP 200の前にリダイレクトがないかを見る

次に、Networkでページの要求を確認します。画面が切り替わってもログを残せるよう、Preserve log を有効にしてから操作します。

私のアプリでは、本番で次のリダイレクトが発生していました。

/drink-session-counter/history.html
  ↓ 308リダイレクト
/drink-session-counter/history
  ↓ 200
履歴ページのHTML

Cloudflare Pagesは、HTMLのURLを拡張子なしの形へリダイレクトします。たとえば /contact.html/contact へ移ります。Cloudflareの配信仕様で説明されている挙動です。

curlなどでリダイレクトを追えば、最終的なHTMLは取得できます。ただし、curlはブラウザのService Workerを通りません。

今回も、サーバーにファイルがあることと、SW経由で画面が開けることは別でした。「200だからページ側は問題ない」と判断すると、間に入っているSWの処理を見落とします。

キャッシュから何を返しているかを確認する

修正前のSWは、すべての要求に対してキャッシュを優先していました。

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

キャッシュがあれば返し、なければネットワークから取得する処理です。短く書けますが、ページ遷移と画像の取得を区別していません。

さらに、インストール時のキャッシュ対象には history.html が入っていました。リダイレクトを追って取得したレスポンスには、その取得履歴が残ります。最終的なステータスが200でも、そのレスポンスをナビゲーションへそのまま返すと、要求の条件によってはブラウザがエラーにします。

自分のアプリで同じ点を調べる場合は、ApplicationCache storage で、遷移先のHTMLがどのURLで保存されているかを確認します。アプリごとにキャッシュを分けているなら、該当するキャッシュを選びます。

SWのfetchハンドラで、respondWith() に渡す直前の値をログに出す方法もあります。requestresponse は、その処理で使っている変数に合わせます。

console.log({
  requestURL: request.url,
  mode: request.mode,
  redirect: request.redirect,
  responseURL: response.url,
  status: response.status,
  redirected: response.redirected
});

redirected: true だけで不具合と決まるわけではありません。ページ遷移の要求に対して、キャッシュとネットワークのどちらから、どのレスポンスを返しているかを照らし合わせます。

履歴画面の不具合で変更したところ

今回は、ページ遷移の要求を request.mode === 'navigate' で分け、ネットワークを優先するようにしました。画像などは従来のキャッシュ優先を残しています。

取得したレスポンスがリダイレクト済みなら、本文・ステータス・ヘッダーから新しい Response を作る処理も加えました。次のコードは、その部分の抜粋です。

const response = await fetch(request);
if (!response.redirected) return response;

return new Response(response.body, {
  status: response.status,
  statusText: response.statusText,
  headers: response.headers
});

元のレスポンスに付いたリダイレクト履歴を、新しいレスポンスへ引き継がないための処理です。キャッシュを優先して即座に返す経路も変えたうえで、2026年9月7日に本番で保存後の履歴画面が開くことを確認しました。

ここで確認したのはオンラインでの復旧です。通信失敗時に戻るキャッシュにも同じ履歴が残っていれば、オフラインでは別途対応が必要です。

修正前後のfetchハンドラ全体と、レスポンスを返せる条件については、Zennに詳しくまとめています。

👉 curlでは200なのに、ブラウザでは開けない。PWAの画面遷移を壊したService Worker

修正後はSWを有効に戻して確認する

Bypass for network を入れたままでは、修正したSWを確認できません。チェックを外し、新しいSWが有効になったことを確かめてから操作します。

ページ側のConsoleでは、現在そのページを制御しているSWを確認できます。

navigator.serviceWorker.controller?.scriptURL

URLが返れば、そのページはSWに制御されています。undefined の場合は、初回アクセスでまだ制御されていないなどの可能性があります。

私のアプリで確認したかったのは、履歴ページ単体の表示だけではありません。「終了して保存」を押し、履歴へ進み、保存した内容を読めるところまでです。

PWAの確認では、初めて開く状態に加えて、SWが登録されている状態でも同じ操作を試す。この一手間が必要でした。

今回の修正対象は、飲み会中に杯数を数え、終了後に記録を見返す小さなツールです。画面や使い方は「何杯飲んだっけ?」の紹介ページに載せています。