跳转到内容

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 写法。

有缓存副本时立即返回;网络响应为下一次请求刷新缓存。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 则避免网络失败变成未处理的拒绝。

处理器让网络请求与 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()。

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-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。

规范

规范状态
无。