登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  业界新闻

浏览器 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()。

安全上下文、Service Worker 控制权、Cache Storage、同源响应、opaque 响应和存储配额之间的原创静态关系说明图
图1:Service Worker 缓存限制的边界说明图,展示安全上下文、控制权、响应类型和存储配额之间的关系;这是原创说明图,不是浏览器截图。

把“新增限制”拆成响应类型和存储配额

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 时无条件清空所有缓存,否则会把更新责任变成每个请求的额外开销。

fetch 事件、Request、Cache.match、网络回退、Response.clone、Cache.put、CACHE_NAME 与 activate 清理之间的原创静态结构图
图2:可回滚缓存策略的结构说明图,展示请求匹配、网络回退、响应复制、写入与旧版本清理的关系;这是原创结构图,不代表一次真实运行。
// 激活新版本时只保留明确允许的缓存名,便于回滚和清理
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 中途接管可能造成资源版本混用。对有强缓存依赖的应用,宁可让用户下一次导航自然切换,也要先把版本兼容关系设计清楚。

一张排查清单:先复现,再决定是否改策略

现象优先检查常见处理
注册成功但页面不走 workerHTTPS、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

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>