You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用浏览器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策略为例,步骤如下:

  1. 当新增缓存前检测到总容量超限,从元数据中取出所有条目,按lastAccessed从小到大排序
  2. 依次删除排序靠前的缓存条目(最近最少访问的),同时删除对应元数据
  3. 重复直到总缓存容量低于阈值,再执行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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 07:53:35