Service Worker 部署后还在发旧 bundle —— 排查与修复

部署完用户还看到旧站点,因为 service worker 拦了请求把旧 bundle 发回去,永远不去拉新的 —— 用 skipWaiting、干净缓存范围、紧急关停一齐处理。

发了一个关键修复,CDN 缓存清完,浏览器硬刷看到新版本,Slack 上用户还在反馈看到老的坏页面,有人几小时都是这样,少数人几天都是。元凶几乎都是某个 PWA、Workbox、Next.js / Astro PWA 插件装上的 service worker:它在网络之前拦住请求,发回缓存里的 index.html,而那份 HTML 引用的旧 JS 哈希块在服务器上可能根本不存在了(直接白屏)。这是”部署完用户仍看到旧版”最常见的一种 bug,需要部署期修复 + 一条给已经被困住用户的逃生通道。

常见原因

按用户反馈出现频率排序。

1. service worker 用 CacheFirst 缓存 index.html

Workbox / vite-plugin-pwa / next-pwa 的默认策略有时会用 CacheFirst 缓存 HTML。一旦进缓存,SW 永远从缓存发 —— 用户永远拿不到引用新 JS 哈希块的新 HTML。

如何识别:DevTools → Application → Service Workers,看 SW 源码。navigationRoute 或 HTML 路由上的 CacheFirst 就是元凶。

2. 新 SW 装上了但没调 skipWaiting()

浏览器静默装了新 SW,它停在 waiting 状态,要等该站点所有 tab 关掉才会激活(生产力工具用户可能挂着几天)。在此之前老 SW 一直控制页面。

如何识别:DevTools → Application → Service Workers 显示 “waiting to activate” 或同时列了两个 SW 版本。“reload to update” 横幅一直不触发。

3. 缓存的 chunk 引用了已不存在的哈希

Workbox 第 N 次构建的 precache manifest 指向 /assets/index.abc123.js,第 N+1 次哈希成 /assets/index.def456.js。SW 发缓存 index.html 引用 abc123,但服务器返回 404,页面直接空白。

如何识别:用户看到空白页;DevTools Network 显示某个 JS chunk 404;SW Application 标签里的 precached 条目指向缺失的哈希。

4. 缓存范围设到根、长期不清

navigator.serviceWorker.register('/sw.js', { scope: '/' }) 控制根下所有 URL。几个月前装的 SW 至今没更新,期间你改过路径、删过路由,SW 还在声称这些路由归它管。

如何识别:本该 404 的路径返回了缓存响应。用户身上的 SW 是几个版本前装的,没有更新过。

5. activate 里没调 clients.claim()

即使新 SW 激活,已经打开的 tab 不会被它接管(要么 reload)。

如何识别:DevTools 里新 SW 显示 “activated” 但页面控制权还在老 SW。刷新一次后新 SW 才接管。

6. CDN 给 sw.js 自己加了长 max-age

sw.js 响应头 Cache-Control: max-age=31536000,浏览器 HTTP 缓存直接命中 SW,根本不去看新版。SW 更新生命周期永远不启动。

如何识别curl -I https://your-site/sw.js 显示长 max-age。浏览器 DevTools Network 显示 sw.js 走 disk cache 而非 network。

7. SW 条件注册,生产上静默失败

if (window.location.hostname !== 'localhost') registerSW() —— 如果只在生产注册,生产 SW install 出 bug(比如 precache 文件缺失)会静默失败,页面看起来正常,但离线支持 / 快速加载没了。

如何识别:DevTools Console 出现 Failed to register a ServiceWorkerService worker installation failed

开始排查前

  • 确认确实装了 SW:DevTools → Application → Service Workers 看注册情况。
  • 确认是哪个库装的:Workbox、next-pwa、vite-plugin-pwa、还是自定义。
  • 抓一份受影响用户的浏览器 console / network 截图(如果能)。
  • 弄清楚当前各路由类型(HTML、JS、图片)的缓存策略。
  • 确认你能立刻部署新版 SW 作为修复手段。

需要收集的信息

  • 生成 SW 用的库名和版本(@vite-pwa/astroworkbox-webpack-pluginnext-pwa)。
  • 配置里按路由的缓存策略(runtimeCaching 数组)。
  • sw.js 自己的 Cache-Control 头。
  • 是否配置了 skipWaiting()clients.claim()
  • 受影响的 UA / 浏览器列表(PWA 场景多为桌面 Chrome、iOS Safari)。
  • 最新部署的 precache manifest 内容。

分步修复

顺序:先止新血、再救已被困用户。

步骤 1:HTML 立刻切到 NetworkFirst

SW 配置里:

// workbox / vite-plugin-pwa
runtimeCaching: [
  {
    urlPattern: ({ request }) => request.mode === "navigate",
    handler: "NetworkFirst",
    options: {
      cacheName: "html-cache",
      networkTimeoutSeconds: 3,
      expiration: { maxEntries: 50, maxAgeSeconds: 60 * 60 * 24 },
    },
  },
],

新部署后 HTML 总是先走网络,离线才用缓存。带哈希文件名的静态 JS / CSS 可以继续 CacheFirst

步骤 2:加上 skipWaitingclientsClaim

源码(或配置)里:

// vite-plugin-pwa
VitePWA({
  registerType: "autoUpdate",
  workbox: {
    clientsClaim: true,
    skipWaiting: true,
  },
});

或自定义 sw.js

self.addEventListener("install", (event) => {
  self.skipWaiting();
});
self.addEventListener("activate", (event) => {
  event.waitUntil(self.clients.claim());
});

注意:忙碌会话上 skipWaiting 可能造成开着的 tab 与新 SW 版本瞬时不一致。内容站可以接受;有重要进行中状态的应用建议改为弹窗提示用户手动刷新。

步骤 3:不让 sw.js 自己被长缓存

Vercel/Netlify 头配置:

// vercel.json
{
  "headers": [
    {
      "source": "/sw.js",
      "headers": [
        { "key": "Cache-Control", "value": "public, max-age=0, must-revalidate" },
        { "key": "Service-Worker-Allowed", "value": "/" }
      ]
    }
  ]
}

SW 文件本身每次访问都必须重新拉,否则更新永不发生。浏览器对 sw.js 有一些特别照顾,但显式 header 能消除所有模糊地带。

步骤 4:给已经被困的用户发一个紧急关停 SW

部署一个会自我卸载的 SW:

// public/sw.js —— 紧急自卸
self.addEventListener("install", () => self.skipWaiting());
self.addEventListener("activate", async (event) => {
  event.waitUntil((async () => {
    const keys = await caches.keys();
    await Promise.all(keys.map((k) => caches.delete(k)));
    const regs = await navigator.serviceWorker.getRegistrations();
    await Promise.all(regs.map((r) => r.unregister()));
    const clients = await self.clients.matchAll();
    clients.forEach((c) => c.navigate(c.url));
  })());
});

临时部署它。任何还有 SW 的用户下次打开页面会触发自卸 + 刷新 + 拿到真 HTML。1-2 周后再换回你正常(已修复)的 SW。

步骤 5:每次部署让 sw.js 字节不同,强制重新注册

浏览器逐字节对比 sw.js,任何一字节差异就触发 install。强制做到这点:

// sw.js 顶部
const BUILD_ID = "__BUILD_ID__"; // 构建时替换为 commit SHA

构建步骤:

sed -i "s/__BUILD_ID__/$(git rev-parse --short HEAD)/g" dist/sw.js

每次部署 sw.js 都字节不同,更新流程总能启动。

步骤 6:页面内加更新提示横幅

如果不开 skipWaiting,可在 waiting SW 出现时弹提示:

// register-sw.ts
import { registerSW } from "virtual:pwa-register";
const updateSW = registerSW({
  onNeedRefresh() {
    if (confirm("New version available. Reload?")) updateSW(true);
  },
});

把”被困用户”变成一键自救,不必动用紧急关停 SW。

步骤 7:审 Service-Worker-Allowed 和 scope

SW 放在 /static/sw.js,但希望它控制 /

Service-Worker-Allowed: /

不加这个头 SW 只能控制 /static/...,多半不是你想要的。相关响应头排查见 robots txt not working

验证

  • 新部署:清浏览器 SW + 缓存,打开站点,Application → Service Workers 立即显示新 SW 已激活。
  • curl -I https://your-site/sw.js 返回 cache-control: max-age=0
  • DevTools Network 每次导航都看到新 HTML,不再 “(from ServiceWorker)” 拉到旧内容。
  • 找人测一次紧急关停 SW,确认一次刷新就能让被困用户恢复。
  • 一周后 Slack 上没人再反馈”还在看老页面”。

长期预防

  • HTML 默认 NetworkFirst,加短 networkTimeoutSeconds;绝不 CacheFirst HTML。
  • skipWaiting 必须搭配 clientsClaim,单独用任一项都会留下尾巴。
  • 托管配置里把 sw.js 钉在 max-age=0,不给自己留忘记的机会。
  • SW 源码里嵌 build ID / commit SHA,每次部署字节都变。
  • 始终给用户加更新提示横幅,给一条”软”逃生路径。
  • 准备好一条紧急自卸 SW 分支随时可发,迟早会用到。

常见坑

  • “为修 bug” 直接禁用 SW —— 已装 SW 的用户在你发自卸 SW 之前一直坏。
  • “为了快” 给 HTML 用 CacheFirst —— 首次加载省 200ms,每次部署给一周的混乱。
  • 忘了 iOS Safari 的 SW 极顽强,iPhone 用户可能几周都不触发更新。相关浏览器状态调试见 vercel 500 errors
  • 有进行中状态的应用上调 skipWaiting() —— 新 SW 半途接管,用户未保存的编辑会丢。
  • 用文件名版本 sw-v2.js 却不卸载老的,两个同时跑,行为更奇怪。

常见问答

Q: 怎么从服务端日志识别哪些用户卡在旧 SW 上?

看 UA + 请求的静态资源版本。如果某用户在请求 build #88 的 chunk 哈希而你现在是 build #112,他就被困了。可以记一个 x-build-id 合成 header 与当前对比。

Q: 干脆不用 service worker 行不行?

不需要离线、不需要快速重访、不需要推送的话,可以直接不要。多数内容站不需要 SW。它只在能抵得过自身复杂度时才值得留。

Q: 为什么 iOS Safari 的用户更容易遇到?

iOS Safari 跨会话保留 SW 更激进,对 skipWaiting() 反应更慢。紧急关停 SW 对 iOS 用户尤其重要。

Q: 紧急关停 SW 会把已经在用的 PWA 装机搞坏吗?

它只卸载 SW。下次用户访问、你已经部署了新好 SW 时,install 流程会从头跑。“Add to Home Screen” 快捷方式仍可用,PWA 功能会恢复。相关白屏症状排查见 static site blank page

标签: #排查 #service-worker #pwa #cache #部署