← 中文资源

简体中文版本

Service Worker — 部署后用户卡在旧版本

修复 Service Worker 缓存让用户长期拿到旧资源的问题,理解更新激活语义,并部署带版本缓存与正确清理。

阅读英文原文

症状

  Application 面板会告诉你更新究竟卡在哪:旧 worker “activated and is running”、新 worker “waiting to activate”——安装成功而激活从未发生,旧者仍控制着打开的客户端,也没人重载页面。同时 Cache Storage 里是上月日期的条目挂在无版本号的缓存名下,fetch 处理器 cache-first 应答——回访用户拿到的永远是冻结的旧外壳,部署了什么都没用,直到硬刷新或清站点数据。

诊断思路

  先查 worker 状态区分 running 与 waiting,waiting 即更新停在激活阶段;再看缓存名与条目日期,无版本号加旧日期加 cache-first 就是冻结供给;最后追更新触发路径——浏览器在导航与 push 时检查 sw.js 字节差异,字节不变则更新流程根本不启动。

根因

  Service Worker 控制页面直到被显式更新:新 worker 并行安装,但只要有客户端被旧者持有就处于 waiting。叠加 cache-first 策略与无版本缓存,回访者拿到冻结旧外壳,更新握手停在 waiting 阶段。

修复与验证

  把更新舞步跳完整:缓存名随部署打版本(app-v42);activate 时删除所有旧版本缓存;HTML 走 network-first(离线回退缓存),仅不可变的带哈希 assets 走 cache-first;新 worker 里 skipWaiting 加 clients.claim 实现即时接管;页面侧在更新就绪时提示重载,而不是在会话中途热交换状态。验证时连续两次部署:新 worker 激活、旧缓存从 Cache Storage 消失、回访客户端一次导航内加载到新版本。skipWalking 配在途页面会把旧代码与新响应混进同一会话——提示重载比热切换安全。

代码示例

Service Workers
  ▸ activated and is running     sw.js  (old)
  ▸ waiting to activate          sw.js  (new)
// 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;
        })
      )
    );
  }
});