Service Worker · 概念
Service worker 缓存策略
发布于
缓存策略是 service worker 的 fetch 处理器用来在 Cache Storage 副本与网络之间取舍、并决定先后顺序的规则。五种有名字的策略覆盖了全部组合;每一种都只是一段很短的 respondWith() 函数体,Workbox 在 workbox-strategies 中把每种策略封装为一个类。底层原语(FetchEvent、caches.match()、cache.put())已属 Baseline 广泛可用(Chrome 40、Firefox 44、Safari 11.1;BCD api.FetchEvent),所以选择的依据是代价,而不是支持度。
每种策略对同样三个问题给出不同回答:第一次查找去哪里、查找失败时怎么办、之后是否更新缓存。
| 策略 | 第一次查找 | 未命中或失败时 | 是否更新缓存 | 读者付出的代价 |
|---|---|---|---|---|
| Cache First | caches.match() |
fetch(),然后 cache.put() |
仅在未命中时 | 过期条目会一直被返回,直到缓存名改变或条目被删除 |
| Network First | fetch() |
caches.match() |
每次成功都更新 | 每次请求都要等一次完整网络往返,离线时还要加上浏览器判定断网的时间 |
| Stale-While-Revalidate | caches.match() |
fetch() |
总是在后台更新 | 首次之后的每次请求都恰好返回一份过期响应 |
| Network Only | fetch() |
失败 | 从不 | 完全没有离线行为 |
| Cache Only | caches.match() |
失败 | 从不 | 任何未预缓存的资源都是硬失败 |
两条机制适用于所有会写缓存的策略。Response 的 body 是只能读一次的流,处理器必须先 clone(),把一份交给 cache.put()、另一份返回;漏掉克隆时 Chromium 会抛出 TypeError: Failed to execute 'put' on 'Cache': Response body is already used。另外 fetch() 只在网络失败时拒绝:404 或 500 会正常兑现,所以不检查 response.ok 就调用 cache.put() 的策略会把错误页缓存起来。
Opaque 响应(以 mode: 'no-cors' 发起的跨源请求)的 status 为 0,body 不可读。它们可以被存储,但 Chromium 在配额核算中把每条 opaque 响应按约 7 MB 计入,以防止通过体积推断跨源内容,因此对第三方字体或图片使用 Cache First 会以远超实际传输字节的速度消耗配额。
Network First 还有一个隐藏的延迟问题:设备处于强制门户或已失效的 Wi-Fi 时,fetch() 不会很快拒绝,而是等到 TCP 超时。应加一个显式超时(AbortSignal.timeout())并把超时视作未命中,这正是 Workbox NetworkFirst 的 networkTimeoutSeconds 选项所做的事。
下面手写的处理器各实现上表中的一行;最后一个示例给出同样路由的 Workbox 写法。
正确克隆的 Stale-While-Revalidate
Section titled “正确克隆的 Stale-While-Revalidate”有缓存副本时立即返回;网络响应为下一次请求刷新缓存。clone() 发生在 put() 之前,返回的是原始 Response。
self.addEventListener('fetch', (event) => { if (event.request.destination !== 'image') return; event.respondWith((async () => { const cache = await caches.open('images-v1'); const cached = await cache.match(event.request); const network = fetch(event.request).then((response) => { if (response.ok) cache.put(event.request, response.clone()); return response; }); event.waitUntil(network.catch(() => {})); // 让 worker 存活到后台刷新完成 return cached ?? network; })());});没有 event.waitUntil(network) 时,浏览器可能在 respondWith() 落定后、后台刷新完成前就终止 worker;catch 则避免网络失败变成未处理的拒绝。
带超时与离线兜底的 Network First
Section titled “带超时与离线兜底的 Network First”处理器让网络请求与 3 秒计时器赛跑;超时或失败则回退到缓存,导航请求再进一步回退到预缓存的 /offline.html。
self.addEventListener('fetch', (event) => { if (event.request.mode !== 'navigate') return; event.respondWith((async () => { const cache = await caches.open('pages-v1'); try { const response = await fetch(event.request, { signal: AbortSignal.timeout(3000) }); if (response.ok) cache.put(event.request, response.clone()); return response; } catch { return (await cache.match(event.request)) ?? cache.match('/offline.html'); } })());});AbortSignal.timeout() 自 Chrome 103、Firefox 100、Safari 16 起可用(BCD api.AbortSignal.timeout_static);更老的 worker 中改为构造 AbortController,在 setTimeout 里调用 abort()。
在依赖策略之前检测 Cache Storage
Section titled “在依赖策略之前检测 Cache Storage”caches 只在安全上下文(HTTPS 与 localhost;BCD api.caches)中暴露,纯 HTTP 页面同样没有 navigator.serviceWorker.register()。依赖预缓存资源的页面应在注册 worker 前检测,worker 则应把缺失的 caches 当作 Network Only 处理。
// 页面中if ('serviceWorker' in navigator && 'caches' in window) { navigator.serviceWorker.register('/sw.js');} else { // 没有 Cache Storage:应用只能在线运行,不要宣称支持离线。}
// worker 中self.addEventListener('fetch', (event) => { if (!('caches' in self)) return; // 直接放行给网络 // …策略代码…});处理器不调用 respondWith() 直接返回,浏览器就会像没有 worker 一样执行请求,这是正确的降级行为。
用 Workbox 写同样的路由
Section titled “用 Workbox 写同样的路由”workbox-strategies 中的每个类对应上表的一行,并以选项形式补上克隆、ok 检查(通过 CacheableResponsePlugin)与超时。
import { registerRoute } from 'workbox-routing';import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';import { CacheableResponsePlugin } from 'workbox-cacheable-response';
registerRoute(({ request }) => request.mode === 'navigate', new NetworkFirst({ cacheName: 'pages', networkTimeoutSeconds: 3 }));registerRoute(({ request }) => request.destination === 'image', new StaleWhileRevalidate({ cacheName: 'images' }));registerRoute(({ url }) => url.pathname.startsWith('/assets/'), new CacheFirst({ cacheName: 'assets', plugins: [new CacheableResponsePlugin({ statuses: [200] })] }));不加 CacheableResponsePlugin 时,CacheFirst 与 StaleWhileRevalidate 默认缓存状态为 200 或 0(opaque)的任何响应;NetworkFirst 只缓存 200。
- Service Workers specification: FetchEvent(w3.org)
- Workbox strategies module(developer.chrome.com)
- Caching(developer.mozilla.org)
- Cache API
- FetchEvent 与路由
- 离线兜底
- Workbox
规范
| 规范 | 状态 |
|---|---|
| 无。 | |