OpenPWAStore
返回 News
Guide · May 20, 2026

Service Worker 的 skipWaiting 需要更新纪律,而非立即重载

强制激活新 service worker 可能导致数据丢失。安全做法如下。

OpenPWA Editorial3 min read
Service Worker 的 skipWaiting 需要更新纪律,而非立即重载 cover

为什么 skipWaiting() 对 PWA 更新很重要

Service worker 有一个故意延迟新版本接管的生命周期:安装中的 worker 在完成安装后才进入 waiting 状态,用户必须关闭所有标签页或手动触发激活才能使其变为 active

这种设计防止会话中出现突兀的中断,但也拖慢了紧急或低风险更新的速度。skipWaiting() 方法通过强制等待中的 worker 立即激活,填补这一空缺。

MDN 的 service worker 生命周期文档指出,skipWaiting() 不是自动的——它需要明确调用,通常与用户提示搭配。

Service Worker 如何工作

三种状态

  1. Installing:Worker 正在下载与解析资源。脚本在此处调用 self.skipWaiting()
  2. Waiting:Worker 已成功安装,但在所有标签关闭之前等待激活。
  3. Active:Worker 正在控制页面并处理 fetch 事件。

自动与手动激活

  • 手动默认:用户必须关闭你 origin 的所有打开标签页, worker 才会激活。
  • skipWaiting():即使标签打开,立即强制激活。

正确实现 skipWaiting()

步骤 1:在 Worker 中调用 skipWaiting()

// sw.js
self.addEventListener('install', (event) => {
  // 预缓存关键资源……
  event.waitUntil(
    caches.open('v1').then((cache) => {
      return cache.addAll([
        '/',
        '/offline.html',
        '/styles.css',
      ]);
    })
  );
});

// 激活行为
self.addEventListener('activate', (event) => {
  // 清理旧缓存
  event.waitUntil(
    caches.keys().then((cacheKeys) => {
      return Promise.all(
        cacheKeys
          .filter((key) => key.startsWith('v') && key !== 'v1')
          .map((key) => caches.delete(key))
      );
    })
  );
});

// 如果你想立即激活(用户控制):
self.addEventListener('install', (event) => {
  const skipWaitingPromise = self.skipWaiting();
  event.waitUntil(skipWaitingPromise);
});

关键:始终在事件监听器中(通常是 install)调用 skipWaiting(),并用 event.waitUntil() 包裹。不要在 fetch 处理或其他事件中随意调用。

步骤 2:在客户端通知用户

if ('serviceWorker' in navigator) {
  let registration;

  navigator.serviceWorker.register('/sw.js').then((reg) => {
    registration = reg;

    // 监听新 worker 进入 waiting 状态
    reg.addEventListener('updatefound', () => {
      const newWorker = reg.installing;

      newWorker.addEventListener('statechange', () => {
        if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
          // 新版本等待中
          showUpdateAvailable();
        }
      });
    });
  });
}

function showUpdateAvailable() {
  const updateButton = document.createElement('button');
  updateButton.textContent = 'Update now';
  updateButton.onclick = () => {
    if (registration && registration.waiting) {
      // 向等待中的 worker 发送消息以 skip waiting
      registration.waiting.postMessage({ type: 'SKIP_WAITING' });
    }
  };

  // 显示非侵入式提示/横幅
  document.body.appendChild(createUpdateUI(updateButton));
}

// 监听 controller change(worker 激活)
navigator.serviceWorker.addEventListener('controllerchange', () => {
  // 理想情况下,提供"刷新"选项或在用户确认后自动刷新
  window.location.reload();
});

步骤 3:在 Worker 中处理 skipWaiting 消息

// sw.js
self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

为什么用消息? 直接从客户端调用 skipWaiting() 不可能——必须向 worker 发送消息。

常见陷阱

1. 不通知用户就自动调用 skipWaiting()

每次更新都调用 skipWaiting() 可能:

  • 打断会话中的表单提交或事务
  • 失效客户端状态(IndexedDB、LocalStorage 假设)
  • 导致布局偏移或竞态条件错误

指导:仅对低风险更新(小 CSS 调整、HTML 模板更改)跳过等待,或经明确用户同意。

2. 未清理旧缓存

新 worker 激活时,你必须清除之前版本的缓存项,避免存储膨胀与服务job资源:

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames.map((cacheName) => {
          if (cacheName !== currentCacheName) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

3. 激活期间竞态条件

如果客户端在 controllerchange 后不立即刷新,它们可能受新 worker 控制,但仍持有旧假设:

  • 缓解:最安全的方式是在 controllerchange 后立即刷新或提示“刷新”。

4. 忽略 naviationPreload

对于高度依赖网络响应的 PWA,skipWaiting() 未与 navigation preload 协调可能导致重复请求或处理失败:

// sw.js
navigationPreload.enable('/lookup?src=' + self.registration.scope);

在客户端用 navigator.serviceWorker.ready 测试可降低竞态风险。

类型化的更新策略

| 更新类型 | 使用 skipWaiting()? | 理由 | |---------|-------------------|------| | 核心bug修复 | ✅ | 立即提升用户体验 | | 低风险 CSS/HTML 调整 | ✅ | 破坏风险极小 | | JavaScript 逻辑变更 | ⚠️ | 可能中断进行中的操作 | | 资源包变更(JS/CSS) | ✅ | 用户立即受益;通过缓存名协调 | | IndexedDB schema 变更 | ⚠️ | 确保迁移路径 | | 离线策略转变 | ⚠️ | 跳过前充分测试 |

用户体验模式

模式 1:"Update Available" 提示

function createUpdateToast() {
  const toast = document.createElement('div');
  toast.className = 'update-toast';
  toast.innerHTML = `
    <span>新版本可用。</span>
    <button id="updateNow">Refresh</button>
  `;
  document.body.appendChild(toast);

  toast.querySelector('#updateNow').onclick = () => {
    if (registration.waiting) {
      registration.waiting.postMessage({ type: 'SKIP_WAITING' });
    }
  };
}

// 每会话仅提示一次
const updateShown = sessionStorage.getItem('updateShown');
if (!updateShown) {
  sessionStorage.setItem('updateShown', 'true');
  createUpdateToast();
}

模式 2:应用内更新横幅

内容型 PWA:

  • 在顶部/底部添加小横幅
  • 包含“Dismiss”与“Update”按钮
  • 通过 LocalStorage 持久化是否忽略

模式 3:低风险更新静默刷新

非事务型应用:

if (registration.waiting && currentVersion === 'newStableVersion') {
  registration.waiting.postMessage({ type: 'SKIP_WAITING' });
  // controllerchange 后允许客户端自动刷新
  navigator.serviceWorker.addEventListener('controllerchange', () => {
    window.location.reload();
  });
}

何时避免:电商、银行、重表单应用。

验证清单

部署自动 skipWaiting() 前:

Worker 侧

  • [ ] skipWaiting() 在事件监听器内调用,并用 event.waitUntil() 包裹
  • [ ] 消息处理器通过 postMessage() 监听 SKIP_WAITING
  • [ ] activate 事件清理旧缓存
  • [ ] 缓存名已版本化(v1v2)以区分资源

客户端侧

  • [ ] updatefound 监听器检测新 worker
  • [ ] 通过非侵入式 UI(提示、横幅)通知用户
  • [ ] 用户接受更新时通过 postMessage 向 waiting worker 发送消息
  • [ ] controllerchange 监听器触发页面刷新或手动通知用户

测试

  • [ ] 打开多标签测试:验证 controllerchange 无需关闭标签即可触发
  • [ ] 测试缓存清理:确认旧版本不累积
  • [ ] 测试会话中表单提交:确保更新中不丢失数据
  • [ ] 测试离线模式:验证新版本正确缓存并可离线工作

不同应用类型

电商

  • ❌ 避免自动 skipWaiting。
  • ✅ 用用户可控刷新,并保留购物车状态。
  • ✅ 协调 indexedDB 迁移。

内容/资讯

  • ✅ 激进更新提升及时性。
  • ✅ 对低风险更改使用静默刷新。
  • ⚠️ 注意阅读状态(文章滚动位置)。

生产力/工具

  • ⚠️ 更新常影响逻辑;要求用户确认。
  • ✅ 对 reload 保留文档状态(例如草稿内容)。

游戏

  • ⚠️ 会话中更新有害;要求会话结束时刷新。
  • ✅ 使用“更新就绪,继续当前局”消息。

浏览器差异

  • ChromeskipWaiting() 广泛支持;在 DevTools Application 标签检查行为。
  • Edge:基于Chromium,与 Chrome 一致。
  • Firefox:支持 skipWaiting(),但存在一些 devcache 细微差异。
  • Safari:Service worker 可用,但激活生命周期略有不同;充分测试。

工具与调试

DevTools Service Worker 面板

  • 检查 installingwaitingactive worker。
  • 通过 UI 手动触发 skipWaiting 进行测试。
  • 通过 console.log() 查看来自 worker 的日志。

Workbox 指导

若使用 Workbox,其 skipWaiting() 设置简化生命周期处理:

workbox.core.clientsClaim();
workbox.precaching.precacheAndRoute([...]);

// 在 worker 中:
self.addEventListener('install', (event) => {
  event.waitUntil(self.skipWaiting());
});

Workbox 的 clientsClaim() 也有助于页面在激活后立即受控。

下一步

  1. 审计当前 service worker 更新流:是手动还是自动?
  2. 确定应用风险剖面:多少进行中操作会中断?
  3. 设计更新 UX 模式:提示?横幅?应用内模态框?
  4. 实现客户端与 worker 之间的消息传递模式。
  5. 在真实设备上测试打开多标签的场景。

目标不是速度本身——而是在不干扰用户工作的前提下,在必要时交付更新。skipWaiting() 是工具,而非默认。