Service Worker 更新为什么用户仍拿旧页面:缓存版本、waiting 与平滑切换
来源:17golang原创
时间:2026-08-11 11:55:10 448浏览 收藏
线上刚发了新版PWA首页,没过多久客服反馈有近半用户看到的还是旧页面。你打开开发者工具很容易被表面现象误导:新的 sw.js 明明已经下载完成,当前页面却还跑在旧Service Worker的管控下,Cache Storage里同时存着新旧两套完整资源。问题根本不是普通用户没手动刷新这么简单,Service Worker的更新、激活、页面接管是三个完全独立的阶段,任意一个环节卡住都会导致旧页面滞留。
线上Service Worker更新后大量用户仍展示旧页面,90%的场景都不是用户刷新的问题,而是新Worker卡在waiting等待阶段、新旧缓存版本命名不兼容、页面接管机制没配置到位这三类原因叠加导致的。
要点速览
- 新Service Worker安装完成后,默认逻辑下会停在
waiting状态,不会立刻接管所有已经打开的页面。 - 缓存命名必须跟随资源版本同步变更,旧缓存的清理操作一定要放在新版本完全验证可用之后再执行。
skipWaiting()和clients.claim()两个API能加快版本切换的速度,但这不代表你可以不做任何提示就强行刷新用户正在操作的页面。- 每次新版本上线验收,至少要检查Worker运行状态、Cache Storage内容、页面控制者归属、异常回滚路径这四个环节。
先理清楚 Service Worker 的完整更新流程
把单次版本发布拆解成四个递进状态,排查问题的效率会高很多:浏览器先检测到新的Worker脚本,新的实例进入安装流程;安装顺利完成后,它大概率先停留在 waiting 状态;等旧的Worker实例完全退出之后,新Worker才会进入激活流程;激活完成后的Worker,还需要通过 clients.claim() 机制才能正式接管对应作用域下的所有页面。以上任意一步没有正常走完,用户都有可能继续访问到旧的缓存资源。

这里很多开发者都会搞混两个概念:「新脚本已经下载完成」和「新页面已经正式生效」完全不是一回事。registration.installing、registration.waiting、registration.active 这几个标识代表的只是Worker自身的运行状态,绝不等于当前正在浏览的页面已经切换到新Worker管控。页面有没有被它控制,要通过 navigator.serviceWorker.controller 字段来确认。
用注册状态快速定位卡顿环节
const registration = await navigator.serviceWorker.register('/sw.js', {
updateViaCache: 'none'
});
if (registration.waiting) {
showUpdateToast('新版本已准备好');
}
console.log({
installing: Boolean(registration.installing),
waiting: Boolean(registration.waiting),
active: Boolean(registration.active),
controller: Boolean(navigator.serviceWorker.controller)
});
updateViaCache: 'none' 配置只会影响浏览器检查Worker脚本本身、以及它导入的子脚本时的缓存策略,完全不能替代业务侧的应用资源版本管理。线上排查的时候,先确认响应头配置、脚本实际内容、Worker注册状态这三项,再去查业务侧的缓存逻辑,不要一上来就让用户清空整个站点的所有数据。
缓存版本和页面切换逻辑要同步验收
缓存的命名建议直接用构建版本号或者代码提交标识拼接,比如 app-cache-20260811-01。在安装阶段把新版本需要用到的所有资源完整写入新缓存,等到激活阶段再精准删除明确属于旧版本的缓存内容。这么做的好处是新Worker安装中途失败的时候,旧缓存还能继续支撑当前页面正常访问,后续要回滚也有可靠的落脚点。

const CACHE_NAME = 'app-cache-20260811-01';
const ASSETS = ['/', '/index.html', '/app.js', '/styles.css'];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS))
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) => Promise.all(
keys.filter((key) => key.startsWith('app-cache-') && key !== CACHE_NAME)
.map((key) => caches.delete(key))
))
);
});
实际项目落地时,带内容哈希的 app.abc123.js 资源可以设置很长的缓存有效期,入口HTML文件和 sw.js 脚本则要配置合理的缓存策略,让浏览器能及时检测到内容更新。最容易出事故的组合是「旧HTML版本 + 新版资源脚本已经被提前清空」:入口引用的静态资源直接找不到,用户哪怕手动刷新页面也只会看到白屏。
平滑切换:把页面刷新的决定权交给用户
后台管理系统、在线编辑器、带表单填写的这类页面,完全不适合做强制自动刷新。新Worker进入 waiting 状态之后,可以在页面上弹出一个轻量提示,告知用户「检测到新版本,刷新后即可使用最新功能」,等用户手动确认之后,再给处于等待状态的Worker发送对应指令。
const waiting = registration.waiting;
document.querySelector('#update').addEventListener('click', () => {
waiting?.postMessage({ type: 'ACTIVATE_NOW' });
});
navigator.serviceWorker.addEventListener('controllerchange', () => {
window.location.reload();
});
self.addEventListener('message', (event) => {
if (event.data?.type === 'ACTIVATE_NOW') {
self.skipWaiting();
}
});
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim());
});
skipWaiting() 可以让新Worker直接跳过等待阶段尽快进入激活流程,clients.claim() 能让所有已经打开、且在当前Worker作用域下的页面快速被新实例接管。两个API搭配使用之后,页面切换的速度会快很多,但你更要在 controllerchange 层做好单例控制,同一次发布流程只触发一次全局刷新,避免多个打开的标签页同时重复触发刷新操作。
发布后的标准验收顺序
- 打开开发者工具的Application / Storage面板,确认新
sw.js的脚本内容里包含本次发布对应的版本标记。 - 找到Service Workers专属面板区域,逐一核对
installing、waiting、active三类状态,确认没有残留的旧Worker实例。 - 打开Cache Storage面板,确认新缓存里已经存好了入口文件和所有带哈希的静态资源,旧版本的缓存没有被提前全部清空。
- 点击页面上的版本更新提示之后,观察
controllerchange、页面自带的版本标记和后续的网络请求,确认刷新后拿到的是完整同一套新版本资源。 - 用之前的上一版静态资源做一次完整的回滚演练,确认旧缓存还能正常支撑页面打开访问。
如果页面仍旧停留在旧版本,先检查当前页面的 controller 字段是否为空,再核对Worker的注册作用域和脚本响应头配置。不要上来就让用户清理全部站点数据,这种操作只会掩盖原本的缓存设计缺陷,完全不能替代规范的版本切换方案。
常见问题:Service Worker 更新排查
我只改了缓存名,为什么用户看到的还是旧页面?
缓存名变更只能说明新Worker有机会写入全新的缓存资源。如果新Worker还卡在 waiting 状态,当前页面的控制权依旧在旧Worker手里,用户自然看不到任何页面变化。
我已经用了 skipWaiting,还有必要搭配 clients.claim 吗?
绝大多数场景下都需要。前者解决的是新Worker跳过等待直接激活的问题,后者解决的是激活完成后页面接管的问题,二者处理的是生命周期里两个完全独立的阶段。
能不能每次发版都直接清空整个 Cache Storage?
非常不建议这么做。你应该按照自己的缓存名前缀精准清理过期的旧版本缓存,同时保留已经经过验证的可回滚资源。全量清空缓存会直接拉低弱网环境下的页面加载成功率、破坏离线可用能力,也会让版本回滚的容错率变得非常差。
把版本更新做成可回滚的标准发布流程
Service Worker更新的核心从来不是把刷新按钮藏起来,而是确保新版本的所有资源先完整安装到位,再用明确的逻辑触发页面切换。缓存命名规则、Worker运行状态、页面控制权归属、回滚资源储备这四项,全都可以在开发者工具里直接完成验收,之后再根据自身业务页面的属性选择自动切换或者用户手动确认切换的模式。这样哪怕某次线上发布出了问题,你也能快速定位问题根源,判断到底是新脚本没同步更新、Worker卡在等待状态,还是资源版本配套逻辑出了问题。
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习