选择缓存策略
发布于 更新于
完成本指南后,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 响应。代价是每次请求都要等一次完整的 网络往返,在不稳定的网络上用户得先等到失败缓存才作答;上面的超时限制了这段等待。
stale-while-revalidate
Section titled “stale-while-revalidate”立即返回缓存的响应,同时在后台刷新缓存供下次使用。
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 版本预缓存新的一套之前什么都不会更新。
为每类资源匹配策略
Section titled “为每类资源匹配策略”| 资源类型 | 策略 | 原因 |
|---|---|---|
| 带哈希文件名的应用外壳(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 体积。
按时间让条目过期
Section titled “按时间让条目过期”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 做这件事。
- 离线策略
- Service worker 缓存策略
- Cache Storage:离线 PWA 背后的请求/响应存储
- Workbox
- Strategies for service worker caching(developer.chrome.com)
- Service worker caching and HTTP caching(web.dev)
- workbox-strategies(developer.chrome.com)
← 返回指南总览。