Cache API 与缓存策略
约 969 字大约 3 分钟
PWACache API
2026-04-08
Cache API
Cache API 提供 Request → Response 的键值存储,是 Service Worker 实现离线的核心存储层。它与 HTTP 缓存、localStorage、IndexedDB 都不同:
- 以 Request 对象(或 URL 字符串)为 key
- 以 Response 对象 为 value
- 全异步、基于 Promise
- 永久存储(除非被浏览器清理或手动删除)
- 同时暴露给主线程(
window.caches)和 SW(self.caches)
CacheStorage
caches 全局对象管理多个 Cache 实例。
// 打开(不存在则创建)
const cache = await caches.open('v1');
// 列出所有 cache 名
const keys = await caches.keys(); // ['v1', 'assets-v2', ...]
// 删除某个 cache
await caches.delete('v0');
// 在所有 cache 中查找匹配
const res = await caches.match('/app.js');Cache 实例方法
const cache = await caches.open('v1');
// 添加(发起请求并缓存响应)
await cache.add('/index.html');
await cache.addAll(['/a.js', '/b.css']);
// 手动放入(用已有 Request/Response)
await cache.put(request, response.clone());
// 查询
const cached = await cache.match(request, {
ignoreSearch: false, // 是否忽略 query string
ignoreMethod: false, // 是否忽略方法(始终匹配 GET)
ignoreVary: false // 是否忽略 Vary 头
});
// 删除
await cache.delete(request);
// 列出所有 key
const reqs = await cache.keys();注意事项
- 只支持 GET 请求:POST/PUT 等会抛出
TypeError - 响应状态码必须是 2xx:
4xx/5xx无法put(但add会直接 reject) - opaque 响应(跨域
no-cors)可以存,但无法读取内容 - Response 是一次性流:同一个 response 读过后不能再
put,必须先clone()
版本管理
通过 cache 名带版本号,发布新版本时清理旧 cache:
const VERSION = 'v3';
const STATIC_CACHE = `static-${VERSION}`;
const RUNTIME_CACHE = `runtime-${VERSION}`;
self.addEventListener('activate', (event) => {
event.waitUntil((async () => {
const keys = await caches.keys();
await Promise.all(
keys
.filter((k) => ![STATIC_CACHE, RUNTIME_CACHE].includes(k))
.map((k) => caches.delete(k))
);
await self.clients.claim();
})());
});五大缓存策略
1. Cache First(缓存优先)
适用:不经常变化的静态资源(字体、图片、第三方库)。
async function cacheFirst(request, cacheName = 'assets') {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
if (cached) return cached;
const response = await fetch(request);
if (response.ok) cache.put(request, response.clone());
return response;
}- ✅ 极快、省流量
- ❌ 更新需要改版本号或手动失效
2. Network First(网络优先)
适用:需要实时数据但也要离线兜底的场景(新闻首页、API)。
async function networkFirst(request, cacheName = 'pages', timeout = 3000) {
const cache = await caches.open(cacheName);
try {
const network = await Promise.race([
fetch(request),
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout))
]);
if (network.ok) cache.put(request, network.clone());
return network;
} catch {
const cached = await cache.match(request);
if (cached) return cached;
throw new Error('Network failed and no cache');
}
}- ✅ 数据新鲜
- ❌ 弱网体验差(需配合 timeout)
3. Stale While Revalidate(边用边更新)
适用:允许短暂过期的数据,对速度和新鲜度都有要求(头像、社交 feed)。
async function staleWhileRevalidate(request, cacheName = 'swr') {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
const networkPromise = fetch(request).then((res) => {
if (res.ok) cache.put(request, res.clone());
return res;
});
return cached || networkPromise;
}- ✅ 首屏秒开 + 后台更新
- ❌ 用户可能看到旧数据一次
4. Network Only
适用:POST 表单、支付、实时接口,绝不能走缓存。
function networkOnly(request) {
return fetch(request);
}5. Cache Only
适用:预缓存的离线兜底资源。
async function cacheOnly(request) {
const res = await caches.match(request);
if (!res) throw new Error('Not in cache');
return res;
}策略选择速查表
| 资源类型 | 推荐策略 |
|---|---|
| HTML 入口文档 | Network First(带 timeout) |
| JS/CSS(带 hash) | Cache First |
| 字体、图标 | Cache First |
| 用户头像 | Stale While Revalidate |
| API GET | Stale While Revalidate 或 Network First |
| API POST/PUT | Network Only |
| 离线兜底页 | Cache Only |
缓存清理与配额
浏览器对每个 origin 有存储配额,可查询:
const { quota, usage } = await navigator.storage.estimate();
console.log(`已用 ${(usage / 1024 / 1024).toFixed(2)} MB / 总 ${(quota / 1024 / 1024).toFixed(2)} MB`);请求持久化存储(避免被浏览器自动清理):
if (navigator.storage?.persist) {
const granted = await navigator.storage.persist();
console.log('持久化存储:', granted);
}限制 runtime cache 大小
Cache API 本身不支持 LRU/TTL,需手动实现:
async function trimCache(cacheName, maxItems) {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
if (keys.length > maxItems) {
await cache.delete(keys[0]); // FIFO
trimCache(cacheName, maxItems); // 递归直到满足
}
}生产环境建议直接用 Workbox 的
ExpirationPlugin,它内置 LRU 与最大条目数/最大存活时间支持。
