Service Worker 无法动态替换 HTML 内容,只能通过版本化缓存名(如 'pages-v2.3.0')+ install 预加载 + fetch 精准匹配实现“伪动态更新”,需同步 HTML、JS/CSS 哈希、SW 缓存名与 HTTP 缓存头。

HTML 文件不能直接被 Service Worker 动态替换
Service Worker 无法在运行时“重写”已缓存的 index.html 内容——它只能决定返回哪个缓存条目,或发起新请求。所谓“动态替换”,本质是缓存版本切换,而非 DOM 层面的实时 patch。
常见错误现象:index.html 更新后离线仍加载旧版;用户刷新页面却看到过期的标题、按钮文案或路由配置。根本原因是缓存键(URL)没变,而 SW 没触发重新 fetch + put。
- 缓存 HTML 必须用
cache.addAll(['/', '/style.css', '/app.js']),不能只缓存/然后靠后续 fetch 补全——否则依赖资源缺失,页面白屏或报net::ERR_FAILED - HTML 缓存必须和 JS/CSS 版本对齐:比如
app-v2.3.0.js对应的index.html里也得引用该哈希文件,否则新逻辑读旧 HTML 结构可能出错 - 不要给 HTML 加查询参数(如
/index.html?v=20260618),不同参数会生成独立缓存项,导致冗余和命中率下降
如何让 HTML 缓存“看起来是动态更新”的
真正可行的策略是“版本化缓存名 + 安装时预加载 + fetch 阶段精准匹配”,不是靠监听 HTML 内容变化。
- 每次发布新 HTML,把缓存名改成带版本号的,例如
'pages-v2.3.0',并在install事件中调用caches.open('pages-v2.3.0').then(cache => cache.addAll(['/'])) -
fetch事件里只对event.request.destination === 'document'的请求走新缓存,其他资源(JS/CSS)按各自缓存名匹配,避免混用 - HTML 本身建议用
Stale-While-Revalidate模式:先返回缓存 HTML,再后台 fetch 新版并存入新缓存,下次访问即生效
activate 阶段清理旧 HTML 缓存的坑
清理不及时会导致多个版本 HTML 共存,浏览器可能随机返回旧版,且旧缓存占用空间无法释放。
立即学习“前端免费学习笔记(深入)”;
- 清理动作必须放在
activate事件,不能放install——否则旧 SW 还在控制页面,caches.delete()会失败并抛TypeError: Request failed - 不能简单遍历所有缓存名删掉非当前版本的,要排除正在使用的缓存(比如页面还在 render 中的
pages-v2.2.0),可用self.clients.matchAll()辅助判断,但更稳妥的是只删明确废弃的命名(如pages-v1.*) - 调用
self.skipWaiting()和self.clients.claim()要成对出现,否则新 SW 可能长期处于 waiting 状态,activate根本不执行
HTML 放进离线包 vs 单独缓存的取舍
把 index.html 打包进 App 内置离线资源,看似省事,实则牺牲了热更新能力——紧急回滚需等 App 发版或用户重启。
- 纯 SW 方案:HTML 必须走网络首次加载(哪怕只 100ms),但可随时发新版,适合业务逻辑频繁迭代的场景
- 混合方案:HTML 不进离线包,但 JS/CSS/图片进;同时用 SW 缓存 HTML 并设短
max-ageHTTP 头,平衡首屏速度与更新及时性 - 真离线优先:HTML 必须和所有静态资源共用同一套版本标识(如构建时注入
window.__BUILD_VERSION = 'v2.3.0'),SW 在fetch里校验该值是否匹配当前缓存名,不匹配则跳转 fallback 页面
caches.open(),而是让 HTML 的版本号、构建产物哈希、SW 缓存名、HTTP 缓存头四者始终同步——漏掉任意一环,用户就会卡在某个中间态,既看不到最新内容,也退不回稳定版本。



















