为什么需要缓存策略
即便使用了 Service Worker,如何准确缓存资源、更新逻辑以及清除过期内容,都是开发者必须应对的挑战。未优化的缓存策略会导致旧资源长期存在、用户获取不到最新内容,甚至出现版本冲突。
本篇将从基础到进阶详解多种常见缓存策略,讨论它们的适用场景、优缺点,并逐步构建一套可扩展的缓存体系。
核心 API 回顾
在开始之前,复习一下 Service Worker 提供的缓存相关 API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const cacheName = 'my-app-v1';
self.addEventListener('install', event => { event.waitUntil( caches.open(cacheName).then(cache => cache.addAll([ '/index.html', '/styles/main.css', '/scripts/app.js' ])) ); });
self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request).then(response => { return response || fetch(event.request); }) ); });
|
caches.match、caches.open、cache.put、cache.delete 是组成缓存策略的基本工具。
策略一:Cache First
最简单的策略:优先从缓存读取,缓存命中则直接返回,否则请求网络并缓存结果。
适用场景:静态资源、图片、第三方库等。
优点:离线可用、速度快。
缺点:更新复杂,需要手动管理缓存版本。
完整实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const cacheName = 'static-v1';
self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request).then(cachedResponse => { if (cachedResponse) { return cachedResponse; } return fetch(event.request).then(networkResponse => { if (networkResponse.status === 200) { const responseClone = networkResponse.clone(); caches.open(cacheName).then(cache => { cache.put(event.request, responseClone); }); } return networkResponse; }); }) ); });
|
策略二:Network First
优先尝试从网络获取,只有在失败时才回退到缓存。
适用于:API 数据、新闻、动态内容。
优点:保证数据最新。
缺点:网络延迟大时体验差;离线时可能无响应(除非缓存了上一次数据)。
实现示例:
1 2 3 4 5 6 7 8 9 10 11
| self.addEventListener('fetch', event => { event.respondWith( fetch(event.request) .then(networkResponse => { const responseClone = networkResponse.clone(); caches.open(cacheName).then(cache => cache.put(event.request, responseClone)); return networkResponse; }) .catch(() => caches.match(event.request)) ); });
|
策略三:Stale-while-revalidate
既返回缓存内容提升速度,同时异步请求最新资源并更新缓存。
适用于需要平衡快速响应与数据更新的场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| self.addEventListener('fetch', event => { event.respondWith( caches.open(cacheName).then(cache => { return cache.match(event.request).then(cachedResponse => { const networkFetch = fetch(event.request).then(networkResponse => { if (networkResponse.status === 200) { cache.put(event.request, networkResponse.clone()); } return networkResponse; }); return cachedResponse || networkFetch; }); }) ); });
|
策略四:Cache and Network Race
同时发起缓存和网络请求,只要先返回者即可,另一个仍然需要写入缓存。
适合对时效要求高,并且可以容忍某些冗余请求的场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| self.addEventListener('fetch', event => { event.respondWith( new Promise((resolve, reject) => { let rejected = false; const abortController = new AbortController();
const failOnce = () => { if (rejected) { reject('both failed'); } else { rejected = true; } };
caches.match(event.request).then(response => { if (response) { resolve(response); } else { failOnce(); } }).catch(failOnce);
fetch(event.request, { signal: abortController.signal }) .then(networkResponse => { resolve(networkResponse); caches.open(cacheName).then(cache => cache.put(event.request, networkResponse.clone())); abortController.abort(); }) .catch(failOnce); }) ); });
|
缓存版本管理与清理
当应用升级时,旧缓存可能过时。常见做法是在 install 或 activate 事件中删除旧缓存:
1 2 3 4 5 6 7 8 9 10
| self.addEventListener('activate', event => { const currentCaches = ['static-v2', 'dynamic-v1']; event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.filter(name => !currentCaches.includes(name)).map(name => caches.delete(name)) ); }) ); });
|
此外,可以为缓存资源设置 maxEntries 与 maxAgeSeconds,通过定期清理确保不占用过多空间。
离线优先策略组合
对于绝大多数前端应用,我们会采用混合策略:
- 基本静态资源 使用 Cache First
- API 数据 使用 Network First 或 Stale-while-revalidate
- 大型媒体 可采用 Cache First 并加上容量控制
通过配置不同的路由规则(如通过 workbox 插件)可简化工
具化配置。下面示例展示 Workbox 的配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| module.exports = { runtimeCaching: [ { urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/, handler: 'CacheFirst', options: { cacheName: 'images-cache', expiration: { maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60, }, }, }, { urlPattern: /^https?:\/\/(api|another)/, handler: 'NetworkFirst', options: { cacheName: 'api-cache', networkTimeoutSeconds: 10, }, }, ], };
|
监控与分析
缺陷的缓存策略常常表现为:
- 用户始终看到旧版本页面
- 缓存不断膨胀导致存储配额耗尽
可通过在 fetch 事件中添加日志、结合 clients.matchAll 检查缓存状态,或者向后端发送异常报告来监控。
进阶技巧
- 缓存优先级:为不同资源设定权重,优先清理低优先级内容。
- 条件更新:根据 HTTP 头部(如
ETag)判断资源是否改变,减少网络开销。
- 外部 CDN 缓存:结合 Service Worker 与 CDN 缓存可以双重加速。
总结
Service Worker 缓存策略不是一刀切,需要根据应用特点灵活选择。本文介绍的几种策略可作为参考,实际项目中常常结合使用,并通过版本控制、监控与清理机制保持缓存健康。掌握这些技巧,可以让你的前端在断网、慢网环境下表现更优秀,同时避免资源过期带来的问题。
继续探索更多缓存优化方案,将显著提升用户体验和系统稳定性。