iOS端PWA缓存50 MiB限制的应对方法探讨
嘿,这个问题我之前做iOS端PWA的时候实打实踩过坑,Safari对Cache API的50MB限制确实挺棘手的,但只要结合几个策略,不仅能保证缓存满了之后应用依然离线可用,还能避免崩溃。下面是我亲测有效的方案:
1. 先给资源做优先级分层,核心资源锁死缓存
首先得把你的资源拆成两类,精准分配缓存策略:
- 核心资源:就是离线必须用到的基础内容——比如启动页HTML、核心业务JS、基础样式CSS、底部导航图标这些,一定要把总大小控制在50MB以内(能压缩就压缩,比如图片转WebP、JS/CSS混淆压缩)。
- 非核心资源:比如非首屏图片、视频、次要页面的静态资源,这些可以缓存在线优先,或者按需加载。
在Service Worker里,用Cache First策略专门守护核心资源,确保它们最先被缓存且不会被轻易清理:
// 安装阶段缓存核心资源 self.addEventListener('install', (event) => { event.waitUntil( caches.open('core-cache-v1') .then((cache) => cache.addAll([ '/', '/css/core.css', '/js/app-core.js', '/icons/main-logo.png' // 其他核心资源... ])) .then(() => self.skipWaiting()) ); }); // 请求阶段优先命中核心缓存 self.addEventListener('fetch', (event) => { const requestUrl = new URL(event.request.url); // 判断是否为核心资源 if (['/', '/css/core.css', '/js/app-core.js'].includes(requestUrl.pathname)) { event.respondWith( caches.match(event.request) .then((response) => response || fetch(event.request)) ); } else { // 非核心资源用Network First,离线时 fallback 到已有缓存 event.respondWith( fetch(event.request) .then((networkResponse) => { // 有剩余空间再缓存非核心资源 caches.open('non-core-cache-v1') .then((cache) => cache.put(event.request, networkResponse.clone())); return networkResponse; }) .catch(() => caches.match(event.request)) ); } });
2. 用IndexedDB存大资源,绕开Cache API限制
Safari的Cache API卡50MB,但IndexedDB的存储限制宽松得多(一般是设备剩余存储空间的50%左右),像视频、大体积图片这类资源,直接存在IndexedDB里,Service Worker拦截请求时从这里读取即可。
举个简单的大图片存储+读取示例:
// 前端页面将大图片存入IndexedDB async function saveLargeImageToIDB(url) { const response = await fetch(url); const blob = await response.blob(); const db = await openDB('large-resources', 1, { upgrade(db) { db.createObjectStore('images'); } }); await db.put('images', blob, url); } // Service Worker从IndexedDB读取资源 self.addEventListener('fetch', (event) => { const requestUrl = new URL(event.request.url); if (requestUrl.pathname.includes('/large-media/')) { event.respondWith( openDB('large-resources', 1) .then((db) => db.get('images', requestUrl.href)) .then((blob) => { if (blob) { return new Response(blob, { headers: { 'Content-Type': 'image/jpeg' } }); } return fetch(event.request); }) ); } });
注意IndexedDB是异步操作,一定要做好异常捕获,避免读取失败导致页面崩溃。
3. 监控缓存空间,主动清理低优先级资源
一定要监听quotaexceeded事件,当缓存满时,主动清理非核心缓存里的旧资源或低优先级资源,保证核心缓存的空间不被占用。
比如按缓存时间排序,删除最早的非核心资源:
self.addEventListener('fetch', (event) => { event.respondWith( fetch(event.request) .then((response) => { return caches.open('non-core-cache-v1') .then((cache) => { // 尝试缓存,捕获配额不足错误 return cache.put(event.request, response.clone()) .catch(async (err) => { if (err.name === 'QuotaExceededError') { // 获取所有缓存条目,按时间排序 const keys = await cache.keys(); keys.sort(async (a, b) => { const aResp = await cache.match(a); const bResp = await cache.match(b); return new Date(aResp.headers.get('date')) - new Date(bResp.headers.get('date')); }); // 删除最早的10个条目(可调整数量) for (let i = 0; i < 10; i++) { await cache.delete(keys[i]); } // 再次尝试缓存 return cache.put(event.request, response.clone()); } throw err; }); }) .then(() => response); }) ); });
另外,在Service Worker的activate事件里定期清理旧缓存版本,避免无效缓存占用空间。
4. 兜底降级策略,绝对避免崩溃
如果碰到极端情况,缓存空间完全不够,核心资源都存不下了,一定要在代码里加try-catch捕获所有缓存相关错误,保证Service Worker不会崩溃,同时给用户友好提示:
self.addEventListener('install', (event) => { event.waitUntil( (async () => { try { const cache = await caches.open('core-cache-v1'); await cache.addAll([/* 核心资源列表 */]); self.skipWaiting(); } catch (err) { // 缓存失败时记录日志,确保Service Worker依然激活 console.error('核心资源缓存失败:', err); self.skipWaiting(); } })() ); });
前端页面也可以检测离线状态和缓存可用性,如果发现离线时核心资源加载失败,显示友好提示:“当前离线功能受限,请尝试连接网络后重新加载”,而不是让页面直接崩溃。
内容的提问来源于stack exchange,提问作者user444002




