WebView 里永远更新不了的 H5,最后发现是旧 PWA 插件在作祟
这个事故从头到尾都很像微信强制缓存:只影响部分用户、怎么发版都不更新、Nginx 怎么配都没用。
直到抓到那句 from service worker,才发现真正的“强制缓存”来自我们早期上线过的 Service Worker。

前情提要:从 WebView 开始,一路误判到“再观察观察”
第一阶段:小程序 WebView 出现“永远是旧页面”
最早是在小程序 WebView 里出现的。表现是:
只有部分设备中招
重进小程序、下拉刷新、甚至重启微信都没用
重新发版了,用户还是旧页面
加了 hash、随机数、时间戳都没有效果
我们当时的默认假设很自然:微信强制缓存策略太凶。于是一路骂微信(私密马赛

第二阶段:各种 Nginx 去缓存都没效果
中途我们尝试了很多“传统的缓存治理手段”:
Cache-Control: no-cache / no-storeExpires -1Pragma: no-cache对
index.html、静态资源分别配置
结果:问题依然存在。。。两个月没彻底解决,就搁置“观察观察”。
第三阶段:公众号 H5 端正式服上线后,我复现到最极端
H5 正式服上线后,我用电脑从公众号菜单进去:
直接看到三个月前的页面
怎么刷新都不变
更要命的是:服务器日志看不到请求,Network 也显示根本没请求服务器
这就导致“网络侧”方案注定无效:因为请求压根没出客户端。
第四阶段:“二战”转折点——把 request header 发给 GPT
我把那条“缓存命中”的完整请求信息贴给 GPT,它提醒:像是 PWA/SW 问题,让我去检查项目里是否启用了 PWA。
然后我检查了一下:
项目早期确实启过
vite-plugin-pwa后来注释掉了
只要用户访问过那段时间的版本,就可能被旧 SW(service worker) 永久控制
为什么疯狂修改 Nginx,用户侧却一点反应没有?
正常请求链路(没有 Service Worker 时)
在这个模式下,调 Nginx、加标签,都有效,因为请求一定会经过它们。
被旧 Service Worker 控制时:
命中缓存时的结果:
请求从 Cache Storage 直接返回,完全绕过 CDN/Nginx/Origin。
所以改 Nginx 头、改缓存策略、甚至服务器发版,用户侧都毫无变化——因为用户根本没和服务器说过话。
为什么“刷新页面”也没用?
页面刷新只会重新触发 fetch,而 fetch 仍然先被 SW 拿到。只要 SW 策略偏“缓存优先”,就会一直命中旧资源。
为什么“注释掉 PWA”会让问题更严重?
“那我把 PWA 插件关掉了,不就没有 SW 了吗?”
但事实是:它注册在用户浏览器里,不在你的代码里。
SW 的“持久在线” vs “发版周期”
一旦你停止发布 SW 脚本(或让它 404 / 被 CDN 长缓存),那些已注册旧 SW 的用户就可能长期停留在“旧 SW + 旧 Cache Storage”的世界里。
解决方案:插入“自毁型 sw.js”,实现无感修复?
其作用如下:
让旧 SW 退出舞台
把此前留下的 Cache Storage 完全清理掉
让页面回到正常更新路径
我们发布的 sw.js 大致就是按这个状态机跑完后消失。
非常关键的一点:新 SW 不实现 fetch handler。
这意味着它不会继续当“缓存层”,清完场就退场。
解决方案:用同名路径把 SW 注销掉
我最终的解决方案是覆盖 sw 的常见路径:
/sw.js/service-worker.js
新脚本的核心动作是:
skipWaiting():让新 SW 立刻激活,不要等旧页面关闭清
Cache Storage:删掉 Workbox precache + 历史遗留缓存clients.claim()+navigate():尽量自动刷新现有页面(更“无感”)unregister():注销自己
还有一个细节:sw.js 必须永远“可更新”
如果 /sw.js 被 CDN/浏览器长缓存住,就会出现:
虽然发了“自毁 SW”
但用户一直拿到旧的
sw.js(或拿不到更新)于是旧 SW 继续控制,修复失败
那才是真滴完啦哈哈哈

所以如果涉及 SW 文件,建议单独设置强制 revalidate(至少 no-cache),并在发布时对该路径做 purge。
评论