PWA缓存策略进阶:打造极速离线体验
PWA缓存策略进阶打造极速离线体验前言大家好我是前端老炮儿今天咱们来聊聊PWA缓存策略那些事儿。你以为PWA就是简单整个Service Worker缓存一下就完事了那你可就太天真了一个好的缓存策略能让你的应用在离线状态下也能飞起来反之则可能让用户体验跌入谷底。为什么缓存策略如此重要想象一下这个场景用户在地铁上打开你的应用网络信号时有时无。如果你的缓存策略做得好用户几乎感觉不到网络波动如果做得不好页面可能半天加载不出来用户直接就把你的应用卸载了。缓存策略的核心目标首次加载速度让用户第一次访问就能快速看到内容离线可用性断网也能正常使用核心功能更新策略确保用户能及时获取最新版本存储空间合理利用有限的缓存空间常见的缓存策略1. Cache First缓存优先这是最常用的策略适用于不常变化的静态资源。self.addEventListener(fetch, (event) { event.respondWith( caches.match(event.request) .then((response) { // 如果缓存中有直接返回缓存 if (response) { return response; } // 否则从网络获取 return fetch(event.request) .then((networkResponse) { // 将新资源存入缓存 caches.open(my-cache).then((cache) { cache.put(event.request, networkResponse.clone()); }); return networkResponse; }); }) ); });适用场景静态资源CSS、JS、图片不常变化的数据缺点更新不及时需要手动清除缓存2. Network First网络优先先从网络获取失败了再用缓存。self.addEventListener(fetch, (event) { event.respondWith( fetch(event.request) .then((networkResponse) { // 更新缓存 caches.open(my-cache).then((cache) { cache.put(event.request, networkResponse.clone()); }); return networkResponse; }) .catch(() { // 网络失败返回缓存 return caches.match(event.request); }) ); });适用场景经常变化的数据需要实时性的内容缺点离线体验不佳3. Stale-While-Revalidate后台更新这是一个聪明的策略先返回缓存同时在后台更新缓存。self.addEventListener(fetch, (event) { event.respondWith( caches.match(event.request).then((cachedResponse) { const fetchPromise fetch(event.request).then((networkResponse) { caches.open(my-cache).then((cache) { cache.put(event.request, networkResponse.clone()); }); return networkResponse; }); // 先返回缓存网络请求在后台进行 return cachedResponse || fetchPromise; }) ); });适用场景对实时性要求不高但需要更新的数据兼顾性能和新鲜度优点快速响应 后台更新用户体验极佳4. Cache Only仅缓存只从缓存获取不请求网络。self.addEventListener(fetch, (event) { event.respondWith(caches.match(event.request)); });适用场景预缓存的资源完全离线的应用5. Network Only仅网络只从网络获取不使用缓存。self.addEventListener(fetch, (event) { event.respondWith(fetch(event.request)); });适用场景敏感数据如支付信息需要最新数据的请求实战组合策略在实际项目中我们通常不会只用一种策略而是根据不同的资源类型采用不同的策略。self.addEventListener(fetch, (event) { const request event.request; // HTML页面网络优先确保获取最新内容 if (request.mode navigate) { event.respondWith(networkFirst(request)); return; } // API请求后台更新策略 if (request.url.includes(/api/)) { event.respondWith(staleWhileRevalidate(request)); return; } // 静态资源缓存优先 event.respondWith(cacheFirst(request)); }); function cacheFirst(request) { return caches.match(request).then((response) { return response || fetch(request).then((res) { caches.open(static-v1).then((cache) { cache.put(request, res.clone()); }); return res; }); }); } function networkFirst(request) { return fetch(request).then((response) { caches.open(pages-v1).then((cache) { cache.put(request, response.clone()); }); return response; }).catch(() { return caches.match(request); }); } function staleWhileRevalidate(request) { return caches.match(request).then((cached) { const networked fetch(request).then((response) { caches.open(api-v1).then((cache) { cache.put(request, response.clone()); }); return response; }); return cached || networked; }); }版本控制与缓存清理缓存策略的另一个重要问题是版本管理。当我们更新资源时如何让用户获取到新版本使用版本化缓存名称const CACHE_VERSIONS { static: static-v2, pages: pages-v1, api: api-v3 }; // 安装时预缓存 self.addEventListener(install, (event) { event.waitUntil( Promise.all([ caches.open(CACHE_VERSIONS.static).then((cache) { return cache.addAll([ /, /index.html, /styles.css, /app.js ]); }), caches.open(CACHE_VERSIONS.pages).then((cache) { return cache.addAll([/about.html]); }) ]) ); }); // 激活时清理旧缓存 self.addEventListener(activate, (event) { event.waitUntil( caches.keys().then((cacheNames) { return Promise.all( cacheNames.filter((cacheName) { // 删除旧版本缓存 return !Object.values(CACHE_VERSIONS).includes(cacheName); }).map((cacheName) { return caches.delete(cacheName); }) ); }) ); });使用Workbox简化缓存管理手动管理缓存策略很繁琐推荐使用Workbox库import { registerRoute } from workbox-routing; import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from workbox-strategies; import { CacheableResponsePlugin } from workbox-cacheable-response; import { ExpirationPlugin } from workbox-expiration; // 缓存静态资源 registerRoute( ({ request }) request.destination style || request.destination script || request.destination image, new CacheFirst({ cacheName: static-resources, plugins: [ new CacheableResponsePlugin({ statuses: [0, 200] }), new ExpirationPlugin({ maxAgeSeconds: 30 * 24 * 60 * 60, // 30天 maxEntries: 100 }) ] }) ); // 缓存API请求 registerRoute( ({ url }) url.pathname.startsWith(/api/), new StaleWhileRevalidate({ cacheName: api-cache, plugins: [ new CacheableResponsePlugin({ statuses: [200] }), new ExpirationPlugin({ maxAgeSeconds: 5 * 60, // 5分钟 maxEntries: 50 }) ] }) ); // 缓存HTML页面 registerRoute( ({ request }) request.mode navigate, new NetworkFirst({ cacheName: pages, plugins: [ new CacheableResponsePlugin({ statuses: [200] }) ] }) );缓存策略最佳实践1. 合理划分缓存将资源按类型和更新频率分类缓存// 长期缓存不常变化的静态资源 const LONG_TERM_CACHE long-term-v1; // 短期缓存经常变化的数据 const SHORT_TERM_CACHE short-term-v1; // 临时缓存临时数据 const TEMP_CACHE temp-v1;2. 设置缓存大小限制使用Workbox的ExpirationPlugin限制缓存大小new ExpirationPlugin({ maxEntries: 50, // 最多缓存50个条目 maxAgeSeconds: 24 * 60 * 60, // 最大缓存时间1天 purgeOnQuotaError: true // 存储配额不足时自动清理 })3. 实现优雅降级确保在不支持Service Worker的浏览器中也能正常工作if (serviceWorker in navigator) { navigator.serviceWorker.register(/sw.js) .then((registration) { console.log(Service Worker registered:, registration); }) .catch((error) { console.error(Service Worker registration failed:, error); }); }4. 提供更新提示当有新版本可用时提示用户刷新页面let newWorker null; self.addEventListener(updatefound, () { newWorker registration.installing; newWorker.addEventListener(statechange, () { if (newWorker.state installed) { if (navigator.serviceWorker.controller) { // 有新版本可用提示用户 showUpdateNotification(); } } }); }); function showUpdateNotification() { if (confirm(发现新版本是否刷新)) { window.location.reload(); } }常见问题与解决方案Q1: 用户反馈应用显示旧内容原因缓存策略过于保守导致更新不及时。解决方案对于需要实时更新的内容使用Network First或Stale-While-Revalidate策略在资源URL中添加版本号或哈希值定期清理过期缓存Q2: 存储空间不足怎么办原因缓存过多导致浏览器存储空间不足。解决方案设置合理的缓存过期时间使用ExpirationPlugin限制缓存大小监听storageQuota事件及时清理缓存Q3: 缓存更新后页面没有变化原因浏览器可能还在使用旧的缓存。解决方案使用版本化的缓存名称在激活事件中删除旧缓存强制刷新CtrlShiftR可以强制获取最新内容总结PWA缓存策略是一个需要精心设计的系统工程没有放之四海而皆准的方案。关键在于理解不同策略的适用场景根据资源类型选择合适的策略做好版本管理和缓存清理使用工具如Workbox简化开发希望今天的分享能帮你打造出更快、更可靠的PWA应用如果你有更好的缓存策略经验欢迎在评论区分享讨论关注我每天分享前端干货让我们一起成长