使用浏览器Cache接口实现LRU/LFU缓存淘汰策略的最佳实践
基于
window.caches实现一致缓存淘汰策略的方案 原生window.caches接口的缓存淘汰逻辑完全由浏览器主导:达到源站专属配额时直接抛出QuotaExceededError,达到浏览器全局存储限制时,不同浏览器会 fallback 到LRU(最近最少使用)或LFU(最不经常使用)策略,没有统一标准。要实现跨浏览器一致的淘汰逻辑,必须在caches上层封装自定义管理机制,以下是具体实现思路和可用工具:
1. 手动维护缓存元数据
自己跟踪每个缓存条目的核心属性,用于判定淘汰优先级:
- 最后访问时间(适配LRU策略)
- 访问次数(适配LFU策略)
- 条目占用空间(用于控制总缓存容量)
可以创建一个独立的Cache实例或用localStorage存储这些元数据,比如每个缓存键对应如下结构:
{ lastAccessed: 1699999999000, // 时间戳 accessCount: 5, // 访问次数 size: 20971520 // 条目大小(字节,20MB) }
2. 封装缓存操作的工具函数
把原生caches的操作(match/put/delete)封装成自定义方法,在每次操作时同步更新元数据:
- 调用自定义
get()方法时,命中缓存后立刻更新该条目的lastAccessed和accessCount - 调用自定义
set()方法前,先检查当前总缓存容量,若超过预设阈值则触发淘汰逻辑
3. 实现自定义淘汰逻辑
以LRU策略为例,步骤如下:
- 当新增缓存前检测到总容量超限,从元数据中取出所有条目,按
lastAccessed从小到大排序 - 依次删除排序靠前的缓存条目(最近最少访问的),同时删除对应元数据
- 重复直到总缓存容量低于阈值,再执行
cache.put()完成新增
示例代码片段(LRU淘汰):
async function evictLRU(cacheName, targetSize) { const cache = await caches.open(cacheName); const metadataCache = await caches.open(`${cacheName}-metadata`); // 获取所有缓存键和对应元数据 const keys = await cache.keys(); const entries = await Promise.all( keys.map(async key => { const metaResp = await metadataCache.match(key.url); const meta = metaResp ? await metaResp.json() : null; return { key, meta }; }) ); // 过滤有效元数据并按最后访问时间排序 const validEntries = entries.filter(e => e.meta).sort((a, b) => a.meta.lastAccessed - b.meta.lastAccessed); let currentTotal = await calculateTotalCacheSize(cacheName); for (const entry of validEntries) { if (currentTotal <= targetSize) break; await cache.delete(entry.key); await metadataCache.delete(entry.key.url); currentTotal -= entry.meta.size; } } // 计算当前缓存总容量 async function calculateTotalCacheSize(cacheName) { const metadataCache = await caches.open(`${cacheName}-metadata`); const metaEntries = await metadataCache.keys(); let total = 0; for (const key of metaEntries) { const resp = await metadataCache.match(key); const meta = await resp.json(); total += meta.size; } return total; }
4. 用Storage Estimate API辅助容量管理
通过navigator.storage.estimate()可以获取当前域名的存储使用情况和总配额,帮你动态调整缓存阈值,避免触发浏览器的强制淘汰:
async function getStorageQuota() { const { usage, quota } = await navigator.storage.estimate(); // 可以设置缓存阈值为总配额的70%,预留空间避免触发浏览器强制淘汰 const safeThreshold = quota * 0.7; return { used: usage, total: quota, safeThreshold }; }
5. 处理QuotaExceededError异常
即使做了前置容量检查,仍可能出现突发的配额超限(比如浏览器临时调整配额),此时要捕获QuotaExceededError,触发淘汰逻辑后重试缓存写入:
async function safePut(cacheName, request, response, meta) { const cache = await caches.open(cacheName); const metadataCache = await caches.open(`${cacheName}-metadata`); try { await cache.put(request, response.clone()); await metadataCache.put(request.url, new Response(JSON.stringify(meta))); } catch (err) { if (err.name === 'QuotaExceededError') { // 触发LRU淘汰 await evictLRU(cacheName, await getStorageQuota().then(q => q.safeThreshold)); // 重试写入 await cache.put(request, response); await metadataCache.put(request.url, new Response(JSON.stringify(meta))); } else { throw err; } } }
关键注意事项
- 元数据本身会占用少量存储,尽量精简字段(比如用数字时间戳代替Date对象)
- 定期校验元数据与实际缓存的一致性:比如初始化时检查元数据中存在的键是否在实际缓存中,清理无效条目
- 超大文件(单文件>100MB)可单独设置淘汰优先级,比如优先淘汰大文件以快速释放空间
内容的提问来源于stack exchange,提问作者Fez Vrasta
相关产品推荐
相关产品推荐

