Skip to content

Cache-first、network-first 与 stale-while-revalidate ​

缓存策略不是“缓存越多越快”,而是在新鲜度、离线能力、首屏延迟之间做选择。先把请求按资源类型分类:带哈希的静态 JS/CSS 可以 cache-first;新闻列表等读请求通常 network-first;头像或文章正文可接受旧内容时使用 stale-while-revalidate;写请求(尤其 POST)默认 network-only,并把离线队列放到后续专题。

1. Cache-first:稳定 shell ​

js
const STATIC = 'shell-v1';
async function cacheFirst(request) {
  const cached = await caches.match(request);
  if (cached) return cached;
  const response = await fetch(request);
  if (response.ok && response.type === 'basic') {
    const cache = await caches.open(STATIC);
    await cache.put(request, response.clone());
  }
  return response;
}

response.clone() 让一个流返回给页面,另一个流写入 Cache API。缓存命中不应绕过 URL/同源判断,也不应把带用户身份的响应混进公共 shell。删除站点缓存后,cache-first 仍要能回退网络,避免“缓存被用户清除后白屏”。

2. Network-first:数据优先 ​

js
const DATA = 'data-v1';
async function networkFirst(request) {
  try {
    const response = await fetch(request);
    if (response.ok && response.type === 'basic') {
      const cache = await caches.open(DATA);
      await cache.put(request, response.clone());
    }
    return response;
  } catch {
    return (await caches.match(request)) || Response.error();
  }
}

这个策略只在网络失败时返回旧数据;因此 UI 应明确显示“离线数据”而不是假装实时。缓存过期、权限变更和用户特定数据必须有额外的 TTL、版本或服务端校验,不能因为能读到旧响应就判定仍有权限。

3. Stale-while-revalidate:先返回,再刷新 ​

js
async function staleWhileRevalidate(request, event) {
  const cached = await caches.match(request);
  const refresh = fetch(request).then(async response => {
    if (response.ok && response.type === 'basic') {
      const cache = await caches.open(DATA);
      await cache.put(request, response.clone());
    }
    return response;
  });
  // 没有缓存时必须等待网络;有缓存时让刷新在后台继续。
  if (cached) {
    event.waitUntil(refresh.catch(() => undefined));
    return cached;
  }
  return refresh;
}

event.waitUntil(refresh) 延长 worker 生命周期,但不改变已经返回给页面的响应。刷新失败不会让已有缓存失效;下一次请求仍可使用旧值。要避免对每个请求都启动刷新,应限制路径,并考虑 ETag/Cache-Control 让服务器参与校验。

4. 一个有边界的路由器 ​

js
self.addEventListener('fetch', event => {
  const { request } = event;
  const url = new URL(request.url);
  if (request.method !== 'GET' || url.origin !== self.location.origin) return;

  if (request.mode === 'navigate') {
    event.respondWith(cacheFirst(request));
  } else if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirst(request));
  } else if (url.pathname.startsWith('/content/')) {
    event.respondWith(staleWhileRevalidate(request, event));
  }
});

路由器的顺序也是契约:导航、API 和内容路径互不重叠时才容易推理。对每类请求分别测试“缓存命中、网络成功、网络失败、缓存为空”四种状态,不能只测在线首次加载。

练习 ​

为 /api/profile 设计策略:要求在线时不能返回超过 5 分钟的个人资料,断网时可以显示最近一次资料但必须标记离线。写出你需要保存的时间戳、响应头或 UI 状态,并解释为什么简单的 cache-first 不满足要求。

下一步 ​

完成练习后学习更新、清理与回滚。

查看本节答案

用心记录每一步成长