Service Worker 缓存策略——离线优先、后台同步与版本治理的工程闭环
Service Worker 缓存策略——离线优先、后台同步与版本治理的工程闭环一、从离线看板到「永远看到旧页面」Service Worker 缓存的陷阱某工业 SaaS 后台上线 PWA 后运营反馈「上周改的按钮文案客户那边今天还没更新」。排查半天发现是 Service Worker 把旧 HTML 死死钉在 Cache Storage 里新版上线后走的是 CacheFirst用户刷新再多遍也是老界面。这事我见过太多团队栽进去——把 Service Worker 当成「加了就快」的银弹却没设计缓存策略与版本治理。Service WorkerSW本质是跑在浏览器后台的代理脚本拦截页面发出的网络请求。它让 PWA 具备离线能力也让 Web 应用能做后台同步与推送。但这份拦截能力是双刃剑策略对了弱网用户秒开策略错了用户被锁在旧版本里出不来。缓存策略错配的后果分三类。其一HTML 走 CacheFirst 导致版本不更新用户永远看到旧页面。其二接口响应被缓存后返回脏数据工单系统显示两小时前的状态。其三旧版 SW 注册的 Cache 永不清理存储配额被无声吃满最终抛 QuotaExceededError。要把 SW 用对必须把三件事一起设计缓存策略请求怎么走、后台同步离线写操作怎么补传、版本治理旧缓存怎么清。三者缺一PWA 在生产环境必然出事。二、请求分流与缓存生命周期SW 的底层拦截机制SW 的拦截发生在 fetch 事件阶段。页面发出请求后SW 先于网络拿到它决定走缓存还是走网络。这套决策由开发者自行实现没有默认策略。Cache Storage 是一个独立的键值存储键是 Request 对象值是 Response 对象按缓存名分桶管理。三大经典策略各自适配不同资源。CacheFirst 优先读缓存命中即返回适合长期不变的静态资源字体、图标、带 hash 的 JS。NetworkFirst 优先走网络失败回退缓存适合 HTML 与关键接口保证用户拿到最新版本。StaleWhileRevalidate 立即返回缓存的同时后台拉取新版本更新缓存适合可容忍短暂时延的资源图片、非关键配置。缓存的生命周期与 SW 一致。SW 安装时触发 install激活时触发 activate。activate 阶段是清理旧版本缓存的唯一可靠时机——此时旧 SW 已退出新 SW 接管可以安全删除上一版本的 Cache 桶。后台同步Background Sync则依赖 sync 事件在网络恢复时由浏览器触发把离线期间暂存的写操作批量补传。综上SW 的分流让每类请求各走各路静态资源走 CacheFirst 极速命中HTML 走 NetworkFirst 保新鲜写操作靠 Background Sync 兜底补传过期缓存在 activate 阶段统一清理。四类策略组合离线可用与实时新鲜才能兼顾。三、生产级 SW 缓存管理器实现下面给出一个可配置的 SW 缓存管理器。它按路由分发策略带网络超时、写操作离线入队以及版本化缓存清理。// sw.ts —— 在 Service Worker 上下文中运行 const SW_VERSION v3; // 每次发布递增用于版本治理 const CACHE_PREFIX app-shell-; // 路由策略表每条规则匹配一类请求并指定策略 interface RouteRule { pattern: RegExp; strategy: cache-first | network-first | stale-while-revalidate; cacheName: string; networkTimeoutMs?: number; // NetworkFirst 超时回退缓存避免弱网卡死 } const ROUTES: RouteRule[] [ // 带 hash 的静态资源长期不变走 CacheFirst { pattern: /\.(?:js|css|woff2|png|svg)$/, strategy: cache-first, cacheName: static }, // HTML 文档必须新鲜走 NetworkFirst 超时回退 { pattern: /\/(?:index\.html)?$/, strategy: network-first, cacheName: doc, networkTimeoutMs: 3000 }, // 非关键配置走 SWR容忍短暂时延 { pattern: /\/api\/config/, strategy: stale-while-revalidate, cacheName: config }, ]; // install预缓存关键壳资源失败不阻断安装后续按需补拉 self.addEventListener(install, (event) { event.waitUntil( caches.open(${CACHE_PREFIX}${SW_VERSION}).then((cache) cache.addAll([/, /index.html]).catch(() { /* 预缓存失败容错不阻塞 */ }) ) ); self.skipWaiting(); // 新 SW 立即接管配合 activate 清理 }); // activate清理旧版本 Cache 桶释放存储配额 self.addEventListener(activate, (event) { event.waitUntil( caches.keys().then((keys) Promise.all( keys .filter((k) !k.endsWith(SW_VERSION)) // 仅保留当前版本 .map((k) caches.delete(k)) ) ) ); self.clients.claim(); // 立即接管已打开页面 }); // fetch按路由分发策略 self.addEventListener(fetch, (event) { const req event.request; // 写操作不走缓存失败则入队 Background Sync if ([POST, PUT, DELETE].includes(req.method)) { event.respondWith(handleWrite(req)); return; } const rule ROUTES.find((r) r.pattern.test(req.url)); if (!rule) return; // 无规则放行走默认网络 event.respondWith( (async () { try { const cache await caches.open(${CACHE_PREFIX}-${rule.cacheName}); switch (rule.strategy) { case cache-first: return await cacheFirst(req, cache); case network-first: return await networkFirst(req, cache, rule.networkTimeoutMs ?? 3000); case stale-while-revalidate: return await swr(req, cache); } } catch { // 策略异常兜底直连网络避免请求整体失败 return fetch(req); } })() ); }); async function cacheFirst(req: Request, cache: Cache): PromiseResponse { const hit await cache.match(req); if (hit) return hit; const fresh await fetch(req); cache.put(req, fresh.clone()); // clone 后写入原响应返回页面 return fresh; } async function networkFirst(req: Request, cache: Cache, timeoutMs: number): PromiseResponse { // 用 AbortController 控制超时弱网下回退缓存避免卡死 const controller new AbortController(); const timer setTimeout(() controller.abort(), timeoutMs); try { const fresh await fetch(req, { signal: controller.signal }); cache.put(req, fresh.clone()); return fresh; } catch { const hit await cache.match(req); if (hit) return hit; throw new Error(网络与缓存均不可用); } finally { clearTimeout(timer); // 无论成功失败都清理定时器避免句柄泄漏 } } async function swr(req: Request, cache: Cache): PromiseResponse { const hit await cache.match(req); // 后台拉新不阻塞当前请求失败静默保留旧缓存 fetch(req).then((fresh) cache.put(req, fresh.clone())).catch(() {}); if (hit) return hit; return fetch(req); // 首次无缓存直连网络 } // 写操作离线入队网络恢复后由 Background Sync 重放 async function handleWrite(req: Request): PromiseResponse { try { return await fetch(req); } catch { const queue await caches.open(${CACHE_PREFIX}-write-queue); // 请求体只能消费一次clone 后存入队列等待重放 const body await req.clone().text(); const staged new Request(req.url, { method: req.method, headers: req.headers, body }); await queue.put(staged, new Response(body)); await (self as any).registration.sync.register(retry-writes); return new Response(离线已暂存待网络恢复后重试, { status: 202 }); } } self.addEventListener(sync, (event) { if (event.tag retry-writes) { event.waitUntil(replayWrites()); } }); async function replayWrites() { const queue await caches.open(${CACHE_PREFIX}-write-queue); const keys await queue.keys(); for (const key of keys) { try { const body (await (await queue.match(key))?.text()) ?? ; await fetch(key.url, { method: key.method, headers: key.headers, body }); await queue.delete(key); // 成功才出队失败留待下次 sync } catch { // 单条失败不阻断后续等待下一次 sync 重试 } } }关键点在于四处。其一路由表驱动策略分发每类资源各走各路HTML 保新鲜、静态走缓存。其二NetworkFirst 用 AbortController 控制超时弱网下回退缓存不卡死。其三写操作失败入队 Background Sync网络恢复后自动重放。其四activate 阶段按版本号清理旧 Cache 桶存储配额不被无声吃满。四、缓存策略的代价容量、一致性与调试复杂度Service Worker 不是免费午餐。Cache Storage 有容量上限。浏览器约为磁盘可用空间的某个比例单 origin 受 OpQuota 约束超限抛 QuotaExceededError。无差别缓存大图与视频会迅速吃满配额。缓存策略必须按资源价值筛选低频资源不该入缓存。某图床站曾把全量原图缓存进 SW两周后配额告警新用户首次访问直接报错。一致性是更隐蔽的代价。CacheFirst 与 SWR 都会让用户在一段时间内看到旧数据。对价格、库存这类强一致场景缓存策略必须配合接口短 TTL 或彻底绕过缓存。某票务系统曾因 SWR 返回旧票价用户下单时才发现已售罄引发投诉。调试复杂度不容忽视。SW 生命周期有 install、waiting、activating、activated 多个状态更新不及时时新代码不生效。开发期需配合 Update on Reload 与 skipWaiting否则容易陷入「改了代码没反应」的困局。生产期则要避免 skipWaiting 滥用防止页面加载到一半被新 SW 接管导致资源错配。HTTPS 是硬性前置。SW 拥有拦截网络请求的权限浏览器强制仅 HTTPS 环境下注册localhost 例外。混合内容HTTPS 页面加载 HTTP 资源会被 SW 拦截后失败需确保全站 HTTPS 化。适用边界PWA 离线应用、弱网优先的内容站、需要后台补传写操作的工单系统收益最高。强一致交易系统、SSR 频繁变更的运营后台、首屏依赖实时数据的看板应谨慎使用或仅缓存静态壳。五、总结Service Worker 缓存策略的核心是「分流」与「治理」两套机制。落地建议第一按资源类型分发策略静态走 CacheFirst、HTML 走 NetworkFirst、可容忍延迟的走 SWR。第二NetworkFirst 必须带超时弱网下回退缓存避免卡死。第三写操作失败入队 Background Sync网络恢复后重放保证离线数据不丢。第四activate 阶段按版本号清理旧 Cache避免配额被无声吃满。最终在离线可用、数据新鲜与存储可控之间取得平衡。这条路在 PWA 与弱网优先场景下能跑通回报是值得的。资料说明本文中的协议、版本、性能、成本和行业趋势应以可核验的一手资料为准。未标注统计口径的比例、时间表和预测仅作工程讨论不应视为行业事实。可参考 0731 资料来源索引并在发布前将具体来源贴到对应断言之后。

相关新闻

Archify:让 AI Agent 直接在对话中生成可交互、可验证架构图的 Skill

Archify:让 AI Agent 直接在对话中生成可交互、可验证架构图的 Skill

Archify:让 AI Agent 直接在对话中生成可交互、可验证架构图的 Skill 核心观点 Archify 的本质不是"又一个画图工具",而是把画架构图这件事变成 AI Agent 的原生能力。它以 agent skill 的形式安装到 Cursor、Claude Code、Codex CLI、OpenC…

2026/10/11 20:27:01 阅读更多 →
智能分析Agent跑通了却不敢上线?先把权限和日志这关过了

智能分析Agent跑通了却不敢上线?先把权限和日志这关过了

聊《一个数据分析项目改成 AI 流程后,最难的部分完全变了》之前,先说一句实在的:别急着背概念,先看它在真实项目里到底解决什么问题。摘要很多数据分析师转做大模型应用时,花大量精力调 Prompt、搭 RAG、调工具调用&am…

2026/10/11 20:27:00 阅读更多 →
同样是写代码,Claude、Gemini、GPT-5.5谁更像“靠谱同事”?

同样是写代码,Claude、Gemini、GPT-5.5谁更像“靠谱同事”?

目标群体属于为那类想要借助大模型来提高效率的码农范畴人群包括程序员, 还有产品经理以及技术团队负责人亦适用于此项, 同样对侧重于关注AI工具实际落地所具备价值之通俗易读普通读者而言也是有所适配的。核心价值说明这篇文章, 不谈论空泛的参数, 只关注写代码时最为实际的几…

2026/10/11 20:27:01 阅读更多 →

最新新闻

Agent技能工程:可验证、可监控、可复用的智能体能力单元设计

Agent技能工程:可验证、可监控、可复用的智能体能力单元设计

1. “agent-skills”不是新词,而是智能体能力工程的实践切口“agent-skills”这个词乍看像某个开源库的包名,或是某次技术分享里一闪而过的术语缩写。但过去两年在多个跨领域项目中反复遇到它——不是作为概念被宣讲,而是作为实际开发中必须拆…

2026/10/11 22:27:17 阅读更多 →
模塑玻璃瓶缺陷识别数据集:28类缺陷与YOLOv5实战

模塑玻璃瓶缺陷识别数据集:28类缺陷与YOLOv5实战

简介:这份资源是面向工业质检与计算机视觉方向的模塑玻璃瓶缺陷识别数据集,适合从事缺陷检测算法研发、YOLO模型训练及产线视觉方案验证的工程师与学习者使用。数据集覆盖黑点、泡泡颈、破损、刮痕、裂缝等28类常见玻璃瓶缺陷,标注信息完整&a…

2026/10/11 22:27:17 阅读更多 →
误差椭圆详解:从协方差阵到点位精度分析

误差椭圆详解:从协方差阵到点位精度分析

1. 为什么笔记十二要单独写误差椭圆误差理论与测量平差基础这门课,大家最熟悉的肯定是协方差传播、权、条件平差、间接平差这些大块头。等这些基础过了之后,随之而来的一个非常实际的问题就是:平差算出的坐标点,到底有多可靠&…

2026/10/11 22:27:17 阅读更多 →
MySQL查询结果加序号全解析:从ROW_NUMBER到用户变量与分组排名

MySQL查询结果加序号全解析:从ROW_NUMBER到用户变量与分组排名

说实话,数据库开发里最容易被低估的需求,就是“给查询结果加个序号”。听起来不就是一列 1、2、3、4 吗?可真到动手写的时候,版本差异、排序稳定性、分页跳号、分组重排,随便一个细节都能让你在测试环境折腾半天。我这…

2026/10/11 22:27:17 阅读更多 →
森林害虫目标检测数据集实战:YOLOv8训练与避坑指南

森林害虫目标检测数据集实战:YOLOv8训练与避坑指南

简介:这份森林害虫目标检测数据集面向林业智能监测、农业AI应用开发及生态科研人员,聚焦松毛虫、松墨天牛、卷叶蛾三类常见且危害严重的森林害虫识别难题。数据集共1715张实际场景采集的JPEG图片,按训练集1199张、验证集257张、测试集259张划…

2026/10/11 22:27:17 阅读更多 →
BNF与EBNF详解:从语法规则到解析器实战

BNF与EBNF详解:从语法规则到解析器实战

看到“BNF、巴科斯-诺尔范式”这个标题,很多刚接触编译原理的人第一反应是:又一个高大上的数学符号体系。但说句实在话,BNF 是我在编译原理里见过的最接地气的工具之一。它本质上就干了一件事——用一套严格、无歧义的规则,告诉计…

2026/10/11 22:26:15 阅读更多 →

日新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →