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,理解这四种策略的原理,你才能在自己的项目里做出正确的选择。
评论区
登录后可评论。












