ローカルポートでアプリ開発時、以前に開発していたアプリの亡霊がブラウザで見えてしまう場合

2026-05-22 10:06 (23 days ago)
Port Haunting
この記事をテーマにした曲を再生

私は普段 SvelteKit でアプリを開発することが多いが、 http://localhost:5173/ を開いてリロードを繰り返していると、今は起動していないはずの別アプリの残骸が表示されたりされなかったりして、開発しづらい状態になることがあった。

原因はだいたい昔のプロジェクトで登録した Service Worker (以下 SW) が localhost:5173 というオリジンスコープで登録されてしまっているためだ。

なぜ起きるのか

SW はオリジン (スキーム + ホスト + ポート) に紐付いて登録される。開発マシンでは複数のアプリが localhost:5173 のような同じポートを使い回すことが多いから、別アプリで一度 SW を登録すると、次に同じポートで別アプリを開いた時に「前のアプリの SW」がリクエストをインターセプトしてキャッシュを返してしまう。

SW は普通にリロードしてもアンインストールされないし、スーパーリロード (Cmd + Shift + R) でもブラウザによっては残る。だから「リロードしても消えない亡霊」になる。

確認方法

ブラウザの開発者ツール → Application タブ → Service Workers を見ると、今表示中のオリジンで動いている SW の一覧が出る。ここに身に覚えのない SW が並んでいたら原因はこれ。

ついでに Application → Storage → Cache Storage も覗いておくと、 SW が溜め込んだキャッシュも見られる。

解決方法

Service Workers のページで該当 SW の Unregister を押せば、その場で亡霊は消える。 Cache Storage に残ったエントリも一緒に消しておくとより確実。

DevTools の Application → Service Workers 画面

SvelteKit で開発マシンの SW を自動的に無効化する

毎回手で Unregister するのは面倒なので、開発モードでは SW を登録しないようにしてしまうほうが楽。さらに「過去に登録された SW が残っていたら自動で剥がす」ところまでやっておくと、別アプリから乗り換えた時の事故もなくなる。

src/lib/registerServiceWorker.ts を作って以下のように書く。

export function registerServiceWorker() {
  if (!('serviceWorker' in navigator)) return;

  // dev mode では SW を登録しない。
  // SW は origin スコープ (localhost:5173 等) で居座るため、
  // 同じポートで別の Vite アプリを開発する時にキャッシュが汚染される。
  if (import.meta.env.DEV) {
    // 過去の dev セッションで登録された SW が残っていたら除去
    navigator.serviceWorker.getRegistrations().then((regs) => {
      regs.forEach((reg) => reg.unregister());
    });
    if (typeof caches !== 'undefined') {
      caches.keys().then((keys) => {
        keys.forEach((key) => caches.delete(key));
      });
    }
    return;
  }

  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/service-worker.js')
      .catch((err) => console.error('SW registration failed:', err));
  });
}

呼び出し側 (+layout.svelte+page.svelteonMount 内):

<script lang="ts">
  import { onMount } from 'svelte';
  import { registerServiceWorker } from '$lib/registerServiceWorker';

  onMount(() => {
    registerServiceWorker();
  });
</script>

要点

  1. import.meta.env.DEV で分岐: Vite (SvelteKit が内部で使っているビルドツール) が提供する環境変数。 npm run dev の時だけ true になる。
  2. dev では unregister + キャッシュ削除まで行う: ただ return するだけだと、 prod ビルドで登録されてから dev に戻ってきた時に SW が居座ったままになる。明示的に剥がすコードを入れておくと、アプリを切り替えた瞬間にクリーンになる。

PWA をやめる時の注意点

途中で「やっぱり PWA いらない」となって SW ファイルごと削除すると、過去に登録されたユーザーのブラウザには SW が残り続けて、古いキャッシュが永久にヒットしてしまう。

その場合は SW ファイル自体は残しつつ、中身を「自分自身を unregister するだけのスタブ」に置き換えておくのが安全。 SW は navigation ごとに更新がチェックされるので、ユーザーが次にアクセスした時に新しいスタブ版が activate されて、 cache と registration が自動でクリーンアップされる。

self.addEventListener('install', () => self.skipWaiting());

self.addEventListener('activate', (event) => {
  event.waitUntil(
    (async () => {
      const keys = await caches.keys();
      await Promise.all(keys.map((key) => caches.delete(key)));
      await self.registration.unregister();
      const clients = await self.clients.matchAll({
        includeUncontrolled: true,
        type: 'window',
      });
      clients.forEach((client) => client.navigate(client.url));
    })()
  );
});

matchAll には { includeUncontrolled: true, type: 'window' } を渡しておくこと。デフォルトの引数だと「この SW がコントロールしているタブ」しか拾えず、まだ古い SW にぶら下がっているタブを取りこぼす。

まとめ

  • リロードで消えない表示は SW を疑う
  • DevTools の Application → Service Workers で確認、必要なら Unregister
  • 開発時は最初から SW を登録しない設定にしておく
  • PWA をやめる時は「自己 unregister するスタブ SW」に置き換えてから消す
評価をお願いします (会員登録・ログイン不要)
まだ評価がありません
著者は、アプリケーション開発会社 Cyberneura を運営しています。
開発相談をお待ちしています。

アーカイブ