Service Worker 中常用的缓存策略有哪些?
Service Worker 中常用的缓存策略有哪些?
Service Worker 的核心能力之一就是拦截网络请求并控制缓存策略。不同的业务场景需要不同的策略组合,选错策略可能导致用户看到过期数据,或者离线时完全不可用。以下是前端面试中必须掌握的六种缓存策略。
Cache First(缓存优先)
优先从缓存读取响应,缓存未命中时才发起网络请求,并将响应存入缓存。
适用场景:静态资源(CSS、JS、字体、图片),这些资源变化频率低,缓存命中率高。
javascriptconst CACHE_NAME = 'static-v1'; self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request) .then(cachedResponse => { if (cachedResponse) { return cachedResponse; } return fetch(event.request).then(networkResponse => { // 将网络响应克隆后存入缓存 const responseToCache = networkResponse.clone(); caches.open(CACHE_NAME).then(cache => { cache.put(event.request, responseToCache); }); return networkResponse; }); }) .catch(() => { // 网络请求也失败时的兜底处理 return new Response('Offline', { status: 503 }); }) ); });
优点:响应速度极快,离线可用 缺点:缓存未更新前用户始终看到旧版本,需配合版本控制机制主动更新
Network First(网络优先)
优先发起网络请求,成功后将响应写入缓存;网络失败时回退到缓存。
适用场景:实时性要求高的数据,如 API 请求、用户配置、动态页面。
javascriptconst CACHE_NAME = 'dynamic-v1'; self.addEventListener('fetch', event => { event.respondWith( fetch(event.request) .then(networkResponse => { const responseToCache = networkResponse.clone(); caches.open(CACHE_NAME).then(cache => { cache.put(event.request, responseToCache); }); return networkResponse; }) .catch(() => { return caches.match(event.request); }) ); });
优点:优先保证数据新鲜度 缺点:网络不稳定时首屏加载慢,无缓存且离线时完全不可用
Stale While Revalidate(先缓存后更新)
立即返回缓存的响应(如果存在),同时在后台发起网络请求更新缓存。下次请求时用户就能看到最新内容。
适用场景:对实时性有一定要求但更看重响应速度的场景,如文章列表、配置信息。
javascriptconst CACHE_NAME = 'swr-v1'; self.addEventListener('fetch', event => { event.respondWith( caches.open(CACHE_NAME).then(cache => { return cache.match(event.request).then(cachedResponse => { const fetchPromise = fetch(event.request).then(networkResponse => { cache.put(event.request, networkResponse.clone()); return networkResponse; }).catch(() => cachedResponse); return cachedResponse || fetchPromise; }); }) ); });
优点:兼顾速度与新鲜度,用户永远不会等待网络 缺点:当前请求返回的可能是过期数据,不适用于对数据一致性要求严格的场景
Cache Only(仅缓存)
只从缓存获取响应,不发起任何网络请求。资源必须在 Service Worker 安装阶段预缓存。
适用场景:App Shell、离线页面、预缓存的静态资源。
javascript// 安装阶段预缓存 self.addEventListener('install', event => { event.waitUntil( caches.open('precache-v1').then(cache => { return cache.addAll([ '/offline.html', '/styles/main.css', '/scripts/app.js' ]); }) ); }); // 仅从缓存读取 self.addEventListener('fetch', event => { event.respondWith(caches.match(event.request)); });
优点:响应最快,完全离线可用 缺点:缓存内容不会自动更新,必须通过更新 Service Worker 重新预缓存
Network Only(仅网络)
只发起网络请求,不读取也不写入缓存。
适用场景:实时性要求极高且不可缓存的操作,如支付请求、敏感数据查询、非 GET 请求。
javascriptself.addEventListener('fetch', event => { // 对于非 GET 请求或不允许缓存的接口,直接走网络 if (event.request.method !== 'GET') { event.respondWith(fetch(event.request)); } });
优点:保证数据始终是最新的 缺点:离线时完全不可用,网络差时体验不佳
自定义策略组合
实际项目中不会只用一种策略,而是根据请求类型和 URL 特征路由到不同的缓存策略:
javascriptself.addEventListener('fetch', event => { const { request } = event; const url = new URL(request.url); // 非 GET 请求走网络 if (request.method !== 'GET') { event.respondWith(fetch(request)); return; } // 静态资源 - Cache First if (isStaticAsset(request)) { event.respondWith(cacheFirst(request)); return; } // API 请求 - Network First if (url.pathname.startsWith('/api/')) { event.respondWith(networkFirst(request)); return; } // HTML 页面 - Stale While Revalidate if (request.mode === 'navigate') { event.respondWith(staleWhileRevalidate(request)); return; } // 默认 - Network First event.respondWith(networkFirst(request)); }); function isStaticAsset(request) { return ['style', 'script', 'image', 'font'].includes(request.destination); }
缓存策略对比
| 策略 | 响应速度 | 数据新鲜度 | 离线可用 | 典型场景 |
|---|---|---|---|---|
| Cache First | 快 | 低 | 是 | 静态资源 |
| Network First | 慢 | 高 | 是 | API 请求 |
| Stale While Revalidate | 快 | 中 | 是 | 配置/列表 |
| Cache Only | 最快 | 最低 | 是 | App Shell |
| Network Only | 最慢 | 最高 | 否 | 支付/写操作 |
缓存更新与版本控制
缓存策略能管好读取,但缓存什么时候失效、如何主动更新,才是面试中容易踩坑的地方。核心思路是通过缓存名称中的版本号来管理:
javascriptconst CACHE_NAME = 'static-v2'; self.addEventListener('activate', event => { // 激活时清理旧版本缓存 event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames .filter(name => name !== CACHE_NAME) .map(name => caches.delete(name)) ); }) ); });
当 Service Worker 文件发生变更时,浏览器会触发更新流程:安装新的 Worker -> 等待旧 Worker 不再控制页面 -> 激活新 Worker 并清理旧缓存。这就是为什么修改缓存版本号能强制刷新缓存。
使用 Workbox 简化缓存管理
Google 的 Workbox 库封装了上述所有策略,生产环境中建议直接使用:
javascriptimport { registerRoute } from 'workbox-routing'; import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies'; // 静态资源 registerRoute( ({ request }) => ['style', 'script', 'image'].includes(request.destination), new CacheFirst({ cacheName: 'static-v1' }) ); // API 请求 registerRoute( ({ url }) => url.pathname.startsWith('/api/'), new NetworkFirst({ cacheName: 'api-v1' }) ); // HTML 页面 registerRoute( ({ request }) => request.mode === 'navigate', new StaleWhileRevalidate({ cacheName: 'pages-v1' }) );
Workbox 还内置了缓存过期清理(ExpirationPlugin)、缓存大小限制、后台同步等能力,比手写策略更健壮。
面试追问方向
- Service Worker 的生命周期:install -> waiting -> activate 三个阶段各做什么?waiting 状态如何跳过?
- 缓存击穿问题:多个请求同时未命中缓存时,如何避免重复网络请求?
- 跨域请求的缓存:opaque response 的限制和注意事项
- 与 HTTP 缓存的区别:Service Worker 缓存优先级高于 HTTP 缓存,两者的协作关系