← Debugging Guides

阅读中文版本

Service Worker — Users Stuck on the Old Version After Deploy

Fix users permanently served stale assets by a Service Worker cache, understand update activation semantics, and deploy versioned caches with proper cleanup.

intermediatedebuggingpwacachingdeployment

Symptoms

  • Deployed changes never appear for some users until they hard-refresh or clear site data
  • The Network panel shows assets served from ServiceWorker cache with old timestamps
  • Skipping waiting is configured, but clients keep running the previous page version indefinitely

Possible causes

  • The worker caches the shell and serves cache-first, so new HTML and assets never reach returning visitors
  • A new worker installs but never activates because an old client keeps it in waiting, and the page never reloads
  • Cache names are unversioned, so the new worker writes into the same cache the old one reads
  • Clients claim and skip-waiting logic is missing or incomplete, leaving the update dance half-finished

Diagnosis steps

Inspect worker state

DevTools → Application → Service Workers → check "activated and is running" vs "waiting to activate"

Interpretation

A waiting worker means the new version installed but the old one still controls clients — the update stalled at activation.

Check cache names and strategy

DevTools → Application → Cache Storage → list cache names and entry dates

Interpretation

Unversioned cache names or old-dated entries under cache-first show returning users get the frozen copy.

Trace the update path

chrome://serviceworker-internals or sw registration update log in DevTools

Interpretation

The browser checks for updates on navigation and push; a byte-identical sw.js never triggers the update flow.

Root cause

Service Workers control pages until explicitly updated: a new worker installs in parallel but waits while any client is held by the old one. Combined with a cache-first strategy and unversioned caches, returning visitors get the frozen old shell, and the update handshake stalls at the waiting stage.

Fix

Version the cache name per deploy, delete old caches during activation, cache-first only immutable hashed assets while network-first (or stale-while-revalidate) the HTML, call skipWaiting in the new worker plus clients.claim when immediate control is wanted, and prompt reload in the page when an update is ready.

Risk note

skipWaiting with in-flight page state can mix old page code with new worker responses mid-session. Prompt a reload instead of hot-swapping stateful sessions.

Verify the fix

Two consecutive deploys show: new worker activates, old caches are gone from Cache Storage, returning clients load the new version within one navigation.

Symptom walkthrough

The Application panel tells you exactly where the update stalled:

Service Workers
  ▸ activated and is running     sw.js  (old)
  ▸ waiting to activate          sw.js  (new)

A waiting worker means install succeeded and activation never happened — the old one still controls open clients, and nothing reloads them. Meanwhile Cache Storage shows entries from last month under an unversioned name, and the fetch handler answers cache-first: returning users receive the frozen shell no matter what was deployed.

Fix in practice

Complete the update dance properly:

// sw.js — versioned cache, activation cleanup
const VERSION = 'v42';
const CACHE = `app-${VERSION}`;

self.addEventListener('install', () => {
  self.skipWaiting();               // take over when ready
});

self.addEventListener('activate', (event) => {
  event.waitUntil((async () => {
    const keys = await caches.keys();
    await Promise.all(
      keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))
    );
    await self.clients.claim();     // control existing pages
  })());
});

self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  if (event.request.mode === 'navigate') {
    // HTML: network-first, fall back to cache offline
    event.respondWith(
      fetch(event.request).catch(() => caches.match(event.request))
    );
    return;
  }
  if (url.pathname.startsWith('/assets/')) {
    // hashed immutable assets: cache-first is safe
    event.respondWith(
      caches.match(event.request).then((hit) =>
        hit ?? fetch(event.request).then((res) => {
          const copy = res.clone();
          caches.open(CACHE).then((c) => c.put(event.request, copy));
          return res;
        })
      )
    );
  }
});

And let the page cooperate — prompt a reload when the update lands rather than hot-swapping state mid-session.

Verification

Deploy twice in a row and verify each time: the new worker reaches activated and is running, the old caches disappear from Cache Storage after activation, and a returning client loads the new version within one navigation. In DevTools, watch the waiting worker disappear as skipWaiting hands over control. Test the offline path explicitly — network-first HTML with a cache fallback must still serve the shell with the network off. For the reload prompt, confirm a stateful session is never hot-swapped: the page shows the update notice and reloads on user action, not mid-flow. Rule out the related failure: a byte-identical sw.js never triggers an update check, so your build must emit a changed worker file per deploy.