浏览器 Service Worker 缓存策略新增限制怎样排查
来源:17golang原创
时间:2026-10-23 06:09:37 361浏览 收藏
我在排查 Service Worker 缓存问题时,最容易误判的一件事,是把“以前能缓存、现在不命中”直接归结为浏览器新增了限制。实际更常见的情况是:站点不再满足安全上下文,Service Worker 没有控制当前页面,跨源响应变成了 opaque,或者新旧缓存版本同时存在。先把这几层拆开,才能知道到底该改策略、改响应头,还是只清理旧缓存。
官方资料:https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
遇到“缓存策略突然失效”,先确认安全上下文、controller、响应类型和 Cache Storage 配额,再看 match 规则与版本清理。不要先把所有请求改成 no-store,也不要用 ignoreSearch 掩盖错误的 URL 设计。
缓存策略突然不生效,先确认是哪一层在限制
Service Worker 和 Cache API 依赖安全上下文,生产环境通常是 HTTPS,本地开发的 http://localhost 由浏览器作为开发便利条件处理。即使脚本注册成功,也要确认当前页面已经被它控制;第一次打开页面时,注册、安装和接管可能还没有完成。
我会先在页面控制台看 navigator.serviceWorker.controller,再在 Application 面板检查注册的 scope、活动 worker 和 Cache Storage。若 controller 为空,优先处理作用域、页面刷新和激活状态,而不是修改 caches.match()。

把“新增限制”拆成响应类型和存储配额
cache.add()、cache.addAll() 和 cache.put() 并不是无条件接收任何响应。跨源请求在没有合适 CORS 配置时可能得到 opaque 响应,脚本无法读取其中的状态码和响应头;这会让“只缓存 2xx、只缓存指定 Content-Type”的判断失去依据。
更容易被忽略的是配额计算。Chrome DevTools 文档提示,opaque 响应在配额统计中可能按带填充的较大占用计算,单个响应的计费下限约为 7 MB;这个数字是浏览器实现说明,不应当当作所有浏览器都相同的通用规范。若缓存的是跨源字体、图片或接口结果,先在 Network 中确认响应类型,再查看 navigator.storage.estimate() 的使用量和上限。
// 只把明确成功、且能读取响应头的同源资源写入缓存
async function cacheSameOrigin(cache, request, response) {
const sameOrigin = new URL(request.url).origin === self.location.origin;
const contentType = response.headers.get("content-type") || "";
if (!sameOrigin || response.type === "opaque" || !response.ok) {
return false;
}
if (!contentType.includes("text/") && !contentType.includes("javascript")) {
return false;
}
// 复制响应,避免缓存读取和页面返回争用同一个 body 流
await cache.put(request, response.clone());
return true;
}
这段判断适合需要可解释性的静态资源策略;如果业务确实要缓存跨源资源,就必须接受 opaque 的可观测性和配额边界,不能只因为 fetch() 没抛异常就认为它适合长期保存。
请求匹配不命中,先查 URL、方法和 Vary
Cache Storage 不是“只要 URL 看起来一样就一定命中”。Cache.match() 默认会考虑请求方法和响应的 Vary,查询参数也参与匹配。于是,/app.js?v=1 与 /app.js?v=2 可能是两个键;把 ignoreSearch: true 打开后虽然能命中旧资源,却可能把本应区分的版本或租户数据混在一起。
排查时建议把请求的 url、method、destination、cache 和响应的 status、type 记录在开发环境日志里。HTTP 4xx/5xx 响应不会自动变成网络异常,不能只在 catch 里处理;先检查 response.ok,再决定是否写入 Cache Storage。
// 只为 GET 请求提供缓存优先、网络回退的资源策略
self.addEventListener("fetch", (event) => {
const request = event.request;
if (request.method !== "GET") {
return;
}
event.respondWith((async () => {
// 默认保留查询参数和 Vary 语义,避免误合并不同版本资源
const cached = await caches.match(request);
if (cached) {
return cached;
}
try {
const network = await fetch(request);
if (network.ok && network.type === "basic") {
const cache = await caches.open("assets-v3");
// 页面拿原响应,缓存拿副本;写缓存失败不应阻断本次网络响应
event.waitUntil(cache.put(request, network.clone()).catch(() => undefined));
}
return network;
} catch (error) {
// 网络失败时返回已有离线资源;没有回退资源就交给浏览器报错
return (await caches.match("/offline.html")) || Response.error();
}
})());
});
正确做法:把缓存策略写成可回滚的版本
如果确认问题来自策略变更,建议为缓存名加版本,例如 assets-v3,并在 activate 阶段删除不在允许列表中的旧缓存。不要在每次 fetch 时无条件清空所有缓存,否则会把更新责任变成每个请求的额外开销。

// 激活新版本时只保留明确允许的缓存名,便于回滚和清理
const CACHE_ALLOW_LIST = ["assets-v3", "pages-v2"];
self.addEventListener("activate", (event) => {
event.waitUntil((async () => {
const cacheNames = await caches.keys();
await Promise.all(cacheNames.map((name) => {
if (!CACHE_ALLOW_LIST.includes(name)) {
// 删除旧版本,避免旧资源和新策略长期并存
return caches.delete(name);
}
return undefined;
}));
// 让新 worker 尽快接管已打开的页面;是否启用要结合应用更新风险评估
await self.clients.claim();
})());
});
更新时还要留意旧 worker 的 waiting 状态。强制 skipWaiting() 可以缩短等待,但如果旧页面已经加载了一部分旧资源,新 worker 中途接管可能造成资源版本混用。对有强缓存依赖的应用,宁可让用户下一次导航自然切换,也要先把版本兼容关系设计清楚。
一张排查清单:先复现,再决定是否改策略
| 现象 | 优先检查 | 常见处理 |
|---|---|---|
| 注册成功但页面不走 worker | HTTPS、scope、controller、激活状态 | 刷新或调整注册路径,确认控制范围 |
| 跨源资源写入后配额很快满 | response.type、资源数量、storage estimate | 减少 opaque 缓存或改用可读的 CORS 响应 |
| 缓存永远不命中 | 查询参数、请求方法、Vary、缓存名 | 固定 URL 规则,谨慎使用匹配选项 |
| 更新后仍返回旧内容 | waiting worker、旧缓存、激活清理 | 版本化缓存并明确接管策略 |
这套顺序的价值在于,先判断“限制发生在哪一层”,再决定是修部署、修响应、修匹配还是修生命周期。若只看到一次 QuotaExceededError 就全面关闭缓存,通常会损失离线能力,却没有解决 opaque 响应或缓存版本管理的问题。
常见问题
浏览器是不是新增了一个统一的 Service Worker 缓存上限?
不能这样概括。Cache Storage 的可用空间和 opaque 响应的配额计费具有浏览器和实现差异;排查时应记录具体浏览器、响应类型、存储估算和错误,再讨论限制。
把 caches.match 的 ignoreSearch 设为 true 能解决版本缓存吗?
它只改变匹配时是否忽略查询串,可能掩盖版本键设计问题。只有当查询参数确实不影响资源内容时才考虑使用,否则应统一资源 URL 或采用明确的缓存名版本。
缓存写入失败要不要让页面请求也失败?
多数静态资源策略可以让页面先拿到网络响应,再用 event.waitUntil() 异步写缓存;但安装阶段的核心资源如果写入失败,可能应该让安装失败并在下一次更新时重试。两者要按资源重要性区分。
官方资料:https://developer.mozilla.org/en-US/docs/Web/API/Cache、https://developer.mozilla.org/en-US/docs/Web/API/Cache/match、https://developer.chrome.com/docs/devtools/progressive-web-apps
-
文章 · 常见问题 | 2个月前 | 常见问题 · 软件下载 · 账号安全 · Adobe Express登录 Adobe Express安装 Adobe Express网页版 Adobe Express PWA Adobe Express更新324 收藏
-
354 收藏
-
214 收藏
-
科技周边 · 业界新闻 | 4个月前 | 安全 · CI/CD · gitHub actions · 业界新闻 · 开发者工具 · 代码审查 供应链安全 业界新闻 GitHub Actions 机器人PR CI安全473 收藏
-
科技周边 · 业界新闻 | 4个月前 | github · gitHub actions · 业界新闻 · GitHub AI代理 GitHub Actions Agentic Workflows CI分析 Issue分流 工程自动化354 收藏
-
306 收藏
-
321 收藏
-
科技周边 · 业界新闻 | 1小时前 | MySQL · 查询优化 · explain index 生成列 MySQL 8.4 generated column JSON_UNQUOTE SHOW WARNINGS127 收藏
-
374 收藏
-
218 收藏
-
400 收藏
-
399 收藏
-
161 收藏
-
219 收藏
-
316 收藏
-
461 收藏
-
329 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习