跳转到内容

选择缓存策略

发布于 更新于

完成本指南后,Service Worker 拦截的每一类请求都有一个点名的策略,五种策略都变成可以直接粘进 sw.js 的普通函数,而且你清楚每一种在新鲜度、速度或离线可用性上的代价。怎么选取决于资源是静态、动态还是时间敏感的, 以及用户能忍受多旧的响应。

你需要一个带 fetch 处理函数的 Service Worker(把请求路由到这些函数的处理函数见 离线策略)。这些函数使用的 Cache 接口与 HTTP 缓存是两回事:Cache-Control 头决定浏览器 HTTP 缓存保留什么,对 cache.put() 存什么、caches.match() 返回多久没有任何影响。

每个函数接收一个 Request、返回一个 Response,所以 fetch 处理函数可以写成 event.respondWith(strategy(event.request))。cache.put() 会消耗响应体,所以每个函数都先 clone() 再存。

先查缓存;只在未命中时走网络,并把结果存起来。

async function cacheFirst(request, cacheName = 'static') {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(cacheName);
cache.put(request, response.clone());
}
return response;
}

用于带哈希版本号、同一 URL 下永不改变的资源(JavaScript 包、CSS、图片、字体)。响应即时且离线可用; 代价是缓存条目在 Service Worker 版本变化或条目被删除之前永远不会刷新。

先试网络;请求失败时回退到缓存副本。

async function networkFirst(request, cacheName = 'dynamic', timeoutMs = 3000) {
const cache = await caches.open(cacheName);
try {
const response = await Promise.race([
fetch(request),
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeoutMs)),
]);
if (response.ok) cache.put(request, response.clone());
return response;
} catch {
const cached = await cache.match(request);
if (cached) return cached;
throw new Error(`no network and no cached copy for ${request.url}`);
}
}

用于在线时需要最新版本、离线时上一次看到的版本仍有用的 HTML 与 API 响应。代价是每次请求都要等一次完整的 网络往返,在不稳定的网络上用户得先等到失败缓存才作答;上面的超时限制了这段等待。

立即返回缓存的响应,同时在后台刷新缓存供下次使用。

async function staleWhileRevalidate(request, cacheName = 'dynamic') {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
const refresh = fetch(request).then((response) => {
if (response.ok) cache.put(request, response.clone());
return response;
});
return cached || refresh;
}

用于要快、要大致新鲜、但落后一个版本也无妨的内容:头像、文章列表、UI 框架。代价是每次加载多一个请求, 以及恰好旧了一次访问的响应。

绝不查缓存。在 fetch 处理函数里这意味着根本不调用 respondWith(),浏览器就像没有 worker 一样处理请求。

self.addEventListener('fetch', (event) => {
if (event.request.method !== 'GET') return; // 仅网络:POST、PUT、DELETE
});

用于非 GET 请求、实时数据,以及任何绝不能过期的内容。按定义它离线时不工作。

只从缓存提供,未命中即失败。

async function cacheOnly(request) {
const cached = await caches.match(request);
if (cached) return cached;
return new Response('Not precached', { status: 504 });
}

用于在 install 中预缓存的资源:应用外壳与离线回退页。代价是下一个 worker 版本预缓存新的一套之前什么都不会更新。

资源类型 策略 原因
带哈希文件名的应用外壳(HTML、CSS、JavaScript) 缓存优先 同一 URL 下不可变;首次访问后即时
图片与字体 缓存优先 很少变化;体积大到重新抓取很伤
导航请求 网络优先,配离线回退页 在线时 HTML 新鲜;离线时用缓存外壳或回退页
用户专属的 API 数据 网络优先 必须新鲜;缓存是离线兜底
共享或半静态的 API 数据 stale-while-revalidate 先从缓存绘制,为下次刷新
POST、PUT、DELETE、WebSocket 升级 仅网络 无法从缓存提供
预缓存的离线页 仅缓存 存在的意义就是回退永不依赖网络

Workbox 在 workbox-strategies 中以类的形式提供同样五种:CacheFirst、NetworkFirst、 StaleWhileRevalidate、NetworkOnly 与 CacheOnly,用 registerRoute() 按路由注册。NetworkFirst 与 NetworkOnly 接受 networkTimeoutSeconds 选项,作用相当于上面的超时;Workbox 的 StaleWhileRevalidate 无论缓存条目多新都会发出重新验证请求。相比自己拥有的手写函数,这个库的代价是一个构建步骤和几 KB 体积。

Cache Storage 自身没有过期机制,一个月前存的响应会被 caches.match() 原样返回。对既非不可变也非关键的资源, 检查响应的 Date 头,把过旧的条目当作未命中。

async function cacheWithExpiry(request, maxAgeSeconds, cacheName = 'dynamic') {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
if (cached) {
const storedAt = new Date(cached.headers.get('date') || 0).getTime();
if (Date.now() - storedAt < maxAgeSeconds * 1000) return cached;
await cache.delete(request);
}
const response = await fetch(request);
if (response.ok) cache.put(request, response.clone());
return response;
}

Date 头由服务器设置,任何一侧的时钟不准都会让过期时间偏移;把自己的时间戳连同缓存键存进 IndexedDB 更麻烦也更精确。Workbox 的 ExpirationPlugin 用 maxAgeSeconds 与 maxEntries 做这件事。

← 返回指南总览。