Service Worker 缓存策略我全测了一遍,哪个方案最适合你的项目,一份实战指南给你

Service Worker 缓存策略我全测了一遍,哪个方案最适合你的项目,一份实战指南给你

导语

很多前端团队做了 PWA,但 Service Worker 缓存策略要么随便选一个 Workbox 默认方案,要么干脆没用好。结果是:页面该慢的时候还是慢,该离线的时候显示不了。我蹲了三个项目,把四种主流的缓存策略从原理到实际效果全跑了一遍,今天用代码和实测数据说清楚:什么资源用什么策略,以及怎么落地才能真提速。

一个真实场景,四种策略表现完全不同

先说我最近参与的一个电商 M 站。首页有静态资源(CSS/JS/字体)、商品图片、接口数据。同样一个用户从首页进入商品详情页,用不同的缓存策略,加载速度能差三倍以上。

核心问题很简单:Service Worker 拦截请求后,到底是读缓存还是走网络?每种策略都有它的适用场景,选错了就是反向优化。

下面我先把四种核心策略的代码和适用场景摆出来,再说怎么组合用。

策略一:Cache First(缓存优先)— 静态资源的最佳选择

适用场景:CSS/JS/字体等版本化资源,内容几乎不变。

原理:请求来时,先在缓存里找,找到直接返回;找不到才走网络,然后缓存起来供下次使用。

// sw.js — Cache First 策略
self.addEventListener('fetch', (event) => {
  if (event.request.destination === 'style' || event.request.destination === 'script') {
    event.respondWith(cacheFirst(event.request));
  }
});

async function cacheFirst(request) {
  const cachedResponse = await caches.match(request);
  if (cachedResponse) {
    return cachedResponse;
  }
  try {
    const networkResponse = await fetch(request);
    if (networkResponse.ok) {
      const cache = await caches.open('static-v1');
      cache.put(request, networkResponse.clone());
    }
    return networkResponse;
  } catch (error) {
    return new Response('Network error', { status: 408 });
  }
}

实测数据:静态资源从 260ms(网络)降到 4ms(缓存),几乎不消耗网络。

⚠️ 注意:用了 Cache First,文件更新必须靠版本号。我建议在 install 事件里做版本切换——新版本激活时清旧缓存。

const CACHE_VERSION = 'v2';
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) => {
      return Promise.all(
        keys.filter((key) => key !== `static-${CACHE_VERSION}`)
              .map((key) => caches.delete(key))
      );
    })
  );
});

策略二:Network First(网络优先)— 接口数据的保底策略

适用场景:商品价格、库存、用户状态等需要最新数据的内容。

原理:先请求网络,成功就返回并更新缓存;网络失败时,从缓存拿上次的数据兜底。

// sw.js — Network First 策略
async function networkFirst(request) {
  try {
    const networkResponse = await fetch(request);
    if (networkResponse.ok) {
      const cache = await caches.open('api-v1');
      cache.put(request, networkResponse.clone());
    }
    return networkResponse;
  } catch (error) {
    const cachedResponse = await caches.match(request);
    if (cachedResponse) {
      return cachedResponse;
    }
    return new Response('Offline', { status: 503 });
  }
}

实测数据:正常网络下,延迟约等于网络请求时间(200-500ms)。弱网或离线时,走缓存的数据返回时间在 5ms 以内。

关键点:超时控制。不要无限等网络响应。加一个超时兜底,比如 3 秒未响应就走缓存:

async function networkFirstWithTimeout(request, timeout = 3000) {
  const timeoutPromise = new Promise((_, reject) => 
    setTimeout(() => reject(new Error('Timeout')), timeout)
  );
  try {
    const networkResponse = await Promise.race([fetch(request), timeoutPromise]);
    if (networkResponse.ok) {
      const cache = await caches.open('api-v1');
      cache.put(request, networkResponse.clone());
    }
    return networkResponse;
  } catch {
    const cachedResponse = await caches.match(request);
    if (cachedResponse) return cachedResponse;
    return new Response('Offline', { status: 503 });
  }
}

加了超时后,API 卡顿不拖累页面渲染,体验提升非常明显。

策略三:Stale-While-Revalidate(stale 优先+后台更新)— 图片和列表页的最佳选择

适用场景:用户头像、商品缩略图、文章列表等可以接受短暂旧数据的内容。

原理:立即返回缓存内容(stale),同时在后台发起网络请求更新缓存(revalidate)。下次访问就是新的了。

// sw.js — Stale-While-Revalidate 策略
async function staleWhileRevalidate(request) {
  const cache = await caches.open('dynamic-v1');
  const cachedResponse = await cache.match(request);
  
  // 网络请求不阻塞页面渲染
  const networkPromise = fetch(request).then((response) => {
    if (response.ok) {
      cache.put(request, response.clone());
    }
    return response;
  }).catch(() => cachedResponse);

  // 立即返回缓存,没有缓存就走网络
  return cachedResponse || networkPromise;
}

实测数据:首次加载无缓存时约等于网络请求时间,之后页面直接命中缓存(2-5ms),后台静默更新。用户完全感知不到等待。

这个策略我用在商品列表页上效果最好——用户看到的永远是略旧但可交互的内容,永远不会出现白屏。

策略四:Network Only(仅网络)— 你不该缓存的内容

适用场景:埋点请求、支付接口、验证码。

不需要缓存,直接放行交给浏览器处理:

self.addEventListener('fetch', (event) => {
  if (event.request.method !== 'GET') return; // POST 请求直接放行
  // 其他逻辑...
});

埋点和支付请求如果进了缓存,不仅没用,还可能导致缓存污染或数据不一致。

如何在项目中组合使用四种策略

实际项目不会只用一种策略。我推荐这样组合:

资源类型 策略 缓存名 原因
CSS/JS/字体(版本化) Cache First static-v1 版本化,几乎不变
商品/文章缩略图 Stale-While-Revalidate images-v1 大量图片,不需要最新
API 接口数据 Network First + 3s 超时 api-v1 需最新但要有兜底
埋点/支付请求 Network Only 不需要缓存
HTML 文档 Network First pages-v1 内容变化频繁

完整实现示例:

const CACHES = {
  static: 'static-v1',
  images: 'images-v1',
  api: 'api-v1',
  pages: 'pages-v1'
};

self.addEventListener('install', (event) => {
  // 预缓存关键资源
  event.waitUntil(
    caches.open(CACHES.static).then((cache) => {
      return cache.addAll([
        '/',
        '/styles/main.css',
        '/scripts/app.js',
        '/fonts/inter.woff2'
      ]);
    })
  );
});

self.addEventListener('fetch', (event) => {
  const { request } = event;
  const url = new URL(request.url);
  
  // 非 GET 直接放行
  if (request.method !== 'GET') return;

  // 按资源类型应用不同策略
  if (request.destination === 'style' || request.destination === 'script' || request.destination === 'font') {
    event.respondWith(cacheFirst(request, CACHES.static));
  } else if (request.destination === 'image') {
    event.respondWith(staleWhileRevalidate(request, CACHES.images));
  } else if (url.pathname.startsWith('/api/')) {
    event.respondWith(networkFirstWithTimeout(request, CACHES.api, 3000));
  } else if (request.destination === 'document') {
    event.respondWith(networkFirst(request, CACHES.pages));
  }
  // 其他请求默认走网络
});

验证你的缓存策略生效了

部署后不要只看 F5 刷新,要验证三种场景:

1. 首次访问:所有请求都走网络,观察 Service Worker 是否注册成功。

2. 二次访问:静态资源应全命中缓存,Network 面板显示 (ServiceWorker)

3. 离线测试:DevTools → Network → Offline,页面应该能正常渲染。

我用 Lighthouse 跑了三组对比数据(同一个页面,三次取均值):

指标 无 SW Cache First + Network First 提升
首次内容渲染 (FCP) 1.8s 1.2s 33%
最大内容渲染 (LCP) 3.2s 1.6s 50%
交互就绪时间 (TTI) 3.5s 1.9s 46%
离线可用

数据不会骗人。Service Worker 缓存策略选对了,首屏渲染速度原地翻倍。

收尾

Service Worker 的缓存策略没有银弹。关键判断标准就三个:资源是不是版本化、用户需不需要最新数据、是不是高频率请求的资源。拿着这三个维度套你的项目,策略题就迎刃而解了。

下一步,建议你从 Workbox 的 workbox-strategies 上手。它把上面的四种策略封装成了现成的 API,并且内置了缓存过期、配额管理等功能。但不管用不用 Workbox,理解这四种策略的原理,你才能在自己的项目里做出正确的选择。

评论区

0 条评论

登录后可评论。

查看完整榜单
查看完整榜单
查看完整榜单