JavaScript 性能优化系列(五)-3:内存优化:缓存策略调整:合理控制缓存大小和有效期

JavaScript性能优化实战 10w+人浏览 453人参与

JavaScript 性能优化系列(五):内存优化

5.3 缓存策略调整:合理控制缓存大小和有效期

缓存是前端性能优化的「双刃剑」——合理的缓存能将重复访问的资源加载时间缩短80%以上,而失控的缓存会成为内存泄漏的「重灾区」。在前端开发中,缓存失控主要表现为两类问题:

  1. 缓存大小失控:无限存储数据导致内存/存储容量耗尽(如localStorage存满大对象、内存缓存无限制增长);
  2. 缓存有效期失控:缓存数据长期不清理,导致「脏数据」(过期数据)干扰业务,或缓存未及时更新引发数据一致性问题。

本节将从「原理解析→策略设计→代码实现→团队落地」四个维度,系统讲解如何通过「大小控制」和「有效期管理」实现「收益最大化、成本最小化」的缓存方案,同时规避内存泄漏风险。

5.3.1 前端缓存的核心原理与失控风险

要合理控制缓存,首先需明确前端常见缓存的特性差异——不同缓存类型的容量、生命周期、读写性能截然不同,错误选型是缓存失控的首要原因。

5.3.1.1 前端常见缓存类型及特性对比

前端缓存可分为「内存级缓存」和「持久化缓存」两大类,每类包含多种实现,其核心特性对比如下:

缓存类型存储位置容量限制生命周期读写性能适用场景风险点
自定义内存缓存内存(堆)无明确限制(受内存影响)页面刷新/进程关闭后失效极快(μs级)高频访问的临时数据(如接口响应、组件状态)无限缓存导致内存泄漏、GC压力增大
sessionStorage磁盘约5MB/域名会话结束(标签页关闭)较快(ms级)会话级临时数据(如表单草稿、临时筛选条件)存大对象导致存储满、未清理残留数据
localStorage磁盘约5MB/域名永久(需手动清理)较快(ms级)低频访问的持久数据(如用户偏好、配置信息)无限存储导致存储满、数据过期未更新
IndexedDB磁盘无明确限制(受磁盘影响)永久(需手动清理)中(10-100ms)大量结构化数据(如离线缓存、历史记录)未清理旧数据导致磁盘占用过高
React.useMemo/Vue.computed内存(堆)组件生命周期内组件卸载后失效极快(μs级)组件内高频计算结果缓存(如列表过滤结果)滥用导致内存冗余、计算逻辑冗余

核心结论

  • 内存级缓存(自定义内存缓存、useMemo)适合「高频临时数据」,但需严格控制大小,避免内存泄漏;
  • 持久化缓存(localStorage、IndexedDB)适合「低频持久数据」,但需管理有效期,避免数据过期;
  • 任何缓存若不控制「大小」和「有效期」,都会从「性能加速器」沦为「内存/存储负担」。

5.3.1.2 缓存失控的三大核心问题

前端缓存失控会引发一系列连锁反应,具体表现为:

问题1:无限缓存导致内存/存储耗尽
  • 内存缓存:若自定义内存缓存(如全局cache对象)无容量限制,持续存储大对象(如10万条商品数据),会导致内存占用持续增长,触发浏览器内存预警,甚至页面崩溃;
  • 持久化缓存localStorage若无限存储数据(如每次请求都缓存接口响应),会很快达到5MB容量限制,导致后续缓存失败,甚至影响其他依赖localStorage的功能。
问题2:有效期缺失导致数据一致性问题
  • 数据过期:缓存数据未设置有效期(如缓存用户信息后,用户修改昵称但缓存未更新),会导致页面显示旧数据,引发业务逻辑错误;
  • 脏数据残留:会话级缓存(如sessionStorage)未在会话结束前清理,若用户复用标签页,会导致旧数据干扰新会话。
问题3:缓存策略与数据特性不匹配
  • localStorage存储高频更新的大对象(如实时股价):localStorage读写性能有限,且无自动失效机制,导致数据更新不及时、内存占用过高;
  • 用内存缓存存储低频持久数据(如用户配置):页面刷新后缓存丢失,需重新请求,失去缓存意义。

5.3.2 缓存大小控制:避免无限增长的核心策略

缓存大小控制的核心目标是「在保证缓存命中率的前提下,将内存/存储占用控制在安全范围」,常见策略包括「容量限制算法」「场景化容量分配」「动态容量调整」三类。

5.3.2.1 经典缓存容量控制算法:LRU与LFU

当缓存达到预设容量时,需通过算法淘汰「价值最低」的缓存项,常见的淘汰算法中,LRU(最近最少使用)LFU(最不经常使用) 最适合前端场景。

1. LRU(最近最少使用):淘汰最久未访问的缓存项

原理:假设「最近访问过的缓存项,未来被访问的概率更高」,当缓存满时,淘汰最久未访问的项。
适用场景:高频临时数据(如接口响应、组件渲染结果),访问模式符合「近期访问优先」。

代码实现(符合谷歌JS规范)

/**
 * LRU缓存类:控制内存缓存大小,淘汰最久未访问的项
 * @template K - 缓存键类型
 * @template V - 缓存值类型
 */
class LRUCache {
  /**
   * 初始化LRU缓存
   * @param {number} maxSize - 最大缓存容量(默认100)
   */
  constructor(maxSize = 100) {
    if (maxSize < 1) throw new Error('maxSize必须大于0');
    this.maxSize = maxSize;
    // 用Map存储缓存(Map的迭代顺序是插入顺序,便于维护访问顺序)
    this.cache = new Map();
  }

  /**
   * 存入缓存:若容量满,淘汰最久未访问的项
   * @param {K} key - 缓存键
   * @param {V} value - 缓存值
   * @param {number} [ttl] - 可选:缓存有效期(毫秒)
   */
  set(key, value, ttl) {
    // 1. 若键已存在,先删除(保证重新插入后成为最新访问项)
    if (this.cache.has(key)) {
      this.cache.delete(key);
    }

    // 2. 存入缓存(包含值和过期时间)
    this.cache.set(key, {
      value,
      expireTime: ttl ? Date.now() + ttl : Infinity // 无ttl则永久有效
    });

    // 3. 若超过最大容量,淘汰最久未访问的项(Map的第一个元素是最久未访问)
    if (this.cache.size > this.maxSize) {
      const oldestKey = this.cache.keys().next().value;
      this.cache.delete(oldestKey);
      console.log(`LRU缓存:淘汰最久未访问项,key=${String(oldestKey)}`);
    }
  }

  /**
   * 获取缓存:若过期或不存在,返回null
   * @param {K} key - 缓存键
   * @returns {V|null} 缓存值(过期/不存在则返回null)
   */
  get(key) {
    if (!this.cache.has(key)) return null;

    // 1. 获取缓存项,检查是否过期
    const entry = this.cache.get(key);
    const now = Date.now();
    if (entry.expireTime < now) {
      this.cache.delete(key);
      console.log(`LRU缓存:项已过期,key=${String(key)}`);
      return null;
    }

    // 2. 重新插入,更新访问顺序(成为最新访问项)
    this.cache.delete(key);
    this.cache.set(key, entry);

    return entry.value;
  }

  /**
   * 删除指定缓存项
   * @param {K} key - 缓存键
   */
  delete(key) {
    this.cache.delete(key);
  }

  /**
   * 清空所有缓存
   */
  clear() {
    this.cache.clear();
    console.log('LRU缓存:已清空所有项');
  }

  /**
   * 获取当前缓存大小
   * @returns {number} 缓存项数量
   */
  getSize() {
    return this.cache.size;
  }
}

// --------------- 使用示例 ---------------
// 1. 初始化容量为10的LRU缓存,用于存储接口响应
const apiCache = new LRUCache(10);

// 2. 存入缓存(设置5分钟有效期)
apiCache.set('products:page1', [{ id: 1, name: '无线耳机' }, /* ... */], 5 * 60 * 1000);

// 3. 获取缓存(自动更新访问顺序)
const products = apiCache.get('products:page1');
if (products) {
  console.log('从缓存获取商品列表,长度:', products.length);
}

// 4. 模拟缓存满时淘汰
for (let i = 0; i < 15; i++) {
  apiCache.set(`key${i}`, `value${i}`);
}
console.log('缓存满后大小:', apiCache.getSize()); // 输出10(淘汰了key0-key4)
2. LFU(最不经常使用):淘汰访问频率最低的缓存项

原理:假设「访问频率高的缓存项,未来被访问的概率更高」,通过统计访问次数,淘汰频率最低的项。
适用场景:访问频率差异明显的数据(如热门商品、高频查询接口)。

代码实现

/**
 * LFU缓存类:控制内存缓存大小,淘汰访问频率最低的项
 * @template K - 缓存键类型
 * @template V - 缓存值类型
 */
class LFUCache {
  /**
   * 初始化LFU缓存
   * @param {number} maxSize - 最大缓存容量(默认100)
   */
  constructor(maxSize = 100) {
    if (maxSize < 1) throw new Error('maxSize必须大于0');
    this.maxSize = maxSize;
    this.cache = new Map(); // 存储缓存项:key → { value, count, expireTime }
    this.freqMap = new Map(); // 存储频率映射:频率 → Set<key>
    this.minFreq = 1; // 当前最小访问频率(优化淘汰效率)
  }

  /**
   * 更新缓存项的访问频率
   * @param {K} key - 缓存键
   */
  #updateFreq(key) {
    const entry = this.cache.get(key);
    if (!entry) return;

    // 1. 从原频率集合中删除
    const oldFreq = entry.count;
    const oldFreqSet = this.freqMap.get(oldFreq);
    oldFreqSet.delete(key);

    // 2. 若原频率是最小频率且集合为空,更新最小频率
    if (oldFreq === this.minFreq && oldFreqSet.size === 0) {
      this.minFreq = oldFreq + 1;
    }

    // 3. 增加访问频率,加入新频率集合
    const newFreq = oldFreq + 1;
    entry.count = newFreq;
    if (!this.freqMap.has(newFreq)) {
      this.freqMap.set(newFreq, new Set());
    }
    this.freqMap.get(newFreq).add(key);
  }

  /**
   * 存入缓存:若容量满,淘汰访问频率最低的项
   * @param {K} key - 缓存键
   * @param {V} value - 缓存值
   * @param {number} [ttl] - 可选:缓存有效期(毫秒)
   */
  set(key, value, ttl) {
    if (this.cache.has(key)) {
      // 键已存在:更新值和频率
      this.cache.set(key, {
        value,
        count: this.cache.get(key).count, // 保留原频率
        expireTime: ttl ? Date.now() + ttl : Infinity
      });
      this.#updateFreq(key);
      return;
    }

    // 容量满:淘汰访问频率最低的项(从minFreq对应的集合中取第一个)
    if (this.cache.size >= this.maxSize) {
      const minFreqSet = this.freqMap.get(this.minFreq);
      const leastFreqKey = minFreqSet.keys().next().value;
      
      // 删除缓存项和频率映射
      minFreqSet.delete(leastFreqKey);
      this.cache.delete(leastFreqKey);
      console.log(`LFU缓存:淘汰频率最低项,key=${String(leastFreqKey)}`);
    }

    // 存入新缓存项(初始频率1)
    const newFreq = 1;
    this.cache.set(key, {
      value,
      count: newFreq,
      expireTime: ttl ? Date.now() + ttl : Infinity
    });
    if (!this.freqMap.has(newFreq)) {
      this.freqMap.set(newFreq, new Set());
    }
    this.freqMap.get(newFreq).add(key);
    this.minFreq = newFreq; // 新项频率为1,更新最小频率
  }

  /**
   * 获取缓存:若过期或不存在,返回null
   * @param {K} key - 缓存键
   * @returns {V|null} 缓存值
   */
  get(key) {
    if (!this.cache.has(key)) return null;

    const entry = this.cache.get(key);
    const now = Date.now();

    // 检查是否过期
    if (entry.expireTime < now) {
      // 清理过期项的频率映射
      const freq = entry.count;
      this.freqMap.get(freq).delete(key);
      this.cache.delete(key);
      console.log(`LFU缓存:项已过期,key=${String(key)}`);
      return null;
    }

    // 更新访问频率
    this.#updateFreq(key);
    return entry.value;
  }

  /**
   * 清空缓存
   */
  clear() {
    this.cache.clear();
    this.freqMap.clear();
    this.minFreq = 1;
    console.log('LFU缓存:已清空所有项');
  }

  /**
   * 获取当前缓存大小
   * @returns {number} 缓存项数量
   */
  getSize() {
    return this.cache.size;
  }
}

// --------------- 使用示例 ---------------
const hotProductCache = new LFUCache(5);

// 存入缓存(模拟热门商品访问)
hotProductCache.set('product:1', { id: 1, name: '无线耳机' });
hotProductCache.set('product:2', { id: 2, name: '智能手表' });

// 高频访问product:1
hotProductCache.get('product:1');
hotProductCache.get('product:1');

// 继续存入3个项,触发淘汰(淘汰频率最低的product:2)
hotProductCache.set('product:3', { id: 3, name: '充电宝' });
hotProductCache.set('product:4', { id: 4, name: '数据线' });
hotProductCache.set('product:5', { id: 5, name: '耳机壳' }); 
// 输出:LFU缓存:淘汰频率最低项,key=product:2

5.3.2.2 场景化缓存容量分配

不同类型的缓存(内存、localStorage、IndexedDB)容量限制不同,需结合场景分配合理容量:

1. 内存缓存(自定义缓存、useMemo
  • 容量建议:不超过当前页面内存占用的20%(如页面初始内存200MB,内存缓存控制在40MB以内);
  • 分配原则
    • 高频临时数据(如列表过滤结果):容量设为「单次最大数据量×5」(如每次过滤返回20条数据,容量设为100条);
    • 组件内缓存(如useMemo):跟随组件生命周期,组件卸载时自动清理,无需额外容量限制。

React组件内存缓存示例

import React, { useMemo, useState, useEffect } from 'react';
import { LRUCache } from './LRUCache';

// 初始化LRU缓存(容量10,存储商品列表过滤结果)
const productFilterCache = new LRUCache(10);

const ProductList = () => {
  const [products, setProducts] = useState([]);
  const [filter, setFilter] = useState({ category: 'all', priceRange: [0, 1000] });

  // 用useMemo缓存过滤结果,同时结合LRU控制全局缓存大小
  const filteredProducts = useMemo(() => {
    // 生成缓存键(基于过滤条件)
    const cacheKey = `filter:${JSON.stringify(filter)}`;
    
    // 1. 尝试从LRU缓存获取
    const cachedResult = productFilterCache.get(cacheKey);
    if (cachedResult) {
      console.log('从LRU缓存获取过滤结果');
      return cachedResult;
    }

    // 2. 无缓存则计算,并存入LRU缓存(设置10分钟有效期)
    const result = products.filter(item => {
      const matchesCategory = filter.category === 'all' || item.category === filter.category;
      const matchesPrice = item.price >= filter.priceRange[0] && item.price <= filter.priceRange[1];
      return matchesCategory && matchesPrice;
    });
    
    productFilterCache.set(cacheKey, result, 10 * 60 * 1000);
    console.log('过滤结果存入LRU缓存');
    return result;
  }, [products, filter]);

  // 组件卸载时清理缓存(避免内存泄漏)
  useEffect(() => {
    return () => {
      productFilterCache.clear();
    };
  }, []);

  return (
    <div className="product-list">
      {/* 过滤控件 */}
      <div className="filter-controls">
        {/* 分类筛选 */}
        <select 
          value={filter.category} 
          onChange={(e) => setFilter({ ...filter, category: e.target.value })}
        >
          <option value="all">全部分类</option>
          <option value="electronics">电子产品</option>
          <option value="clothing">服装</option>
        </select>
        {/* 价格筛选 */}
        {/* ... */}
      </div>
      
      {/* 商品列表 */}
      <div className="items">
        {filteredProducts.map(item => (
          <div key={item.id} className="product-item">
            <h3>{item.name}</h3>
            <p>¥{item.price}</p>
          </div>
        ))}
      </div>
    </div>
  );
};

export default ProductList;
2. 持久化缓存(localStorage/sessionStorage)
  • 容量限制:严格控制在5MB以内(浏览器标准容量),建议预留20%空间(实际使用不超过4MB);
  • 分配原则
    • 按数据类型分配:用户配置(100KB)、历史记录(1MB)、临时表单(500KB);
    • 避免存储大对象(如超过100KB的数组),改用IndexedDB。

localStorage容量控制封装

/**
 * 带容量控制的localStorage封装
 */
const LocalStorage = {
  /**
   * 计算localStorage当前占用大小(字节)
   * @returns {number} 占用大小(字节)
   */
  #getUsedSize() {
    let size = 0;
    for (let i = 0; i < localStorage.length; i++) {
      const key = localStorage.key(i);
      const value = localStorage.getItem(key);
      // 每个键值对的大小 = 键长度 + 值长度(UTF-16编码,每个字符2字节)
      size += (key.length + value.length) * 2;
    }
    return size;
  },

  /**
   * 检查容量是否充足
   * @param {string} value - 待存储的值
   * @returns {boolean} 是否充足
   */
  #isCapacityEnough(value) {
    const maxSize = 4 * 1024 * 1024; // 4MB(预留1MB空间)
    const valueSize = value.length * 2;
    const usedSize = this.#getUsedSize();
    return usedSize + valueSize <= maxSize;
  },

  /**
   * 清理最旧的缓存项(按存储时间排序)
   * @param {number} needFreeSize - 需要释放的空间(字节)
   */
  #cleanOldItems(needFreeSize) {
    // 1. 获取所有缓存项及存储时间(假设key包含时间戳,如"user:1620000000000")
    const items = [];
    for (let i = 0; i < localStorage.length; i++) {
      const key = localStorage.key(i);
      const value = localStorage.getItem(key);
      // 从key提取时间戳(需约定key格式:"prefix:timestamp:id")
      const timestampMatch = key.match(/:(\d+):/);
      const timestamp = timestampMatch ? Number(timestampMatch[1]) : 0;
      items.push({ key, value, timestamp, size: (key.length + value.length) * 2 });
    }

    // 2. 按时间戳排序(旧→新),依次删除直到释放足够空间
    let freedSize = 0;
    items.sort((a, b) => a.timestamp - b.timestamp).forEach(item => {
      if (freedSize >= needFreeSize) return;
      localStorage.removeItem(item.key);
      freedSize += item.size;
      console.log(`localStorage:清理旧项,key=${item.key},释放空间=${item.size}字节`);
    });

    // 3. 若仍无法释放足够空间,抛出错误
    if (freedSize < needFreeSize) {
      throw new Error('localStorage容量不足,无法清理足够空间');
    }
  },

  /**
   * 存入localStorage(带容量控制)
   * @param {string} key - 缓存键(建议格式:"prefix:timestamp:id")
   * @param {any} value - 缓存值(自动JSON.stringify)
   * @param {number} [ttl] - 有效期(毫秒,过期后get时自动删除)
   */
  set(key, value, ttl) {
    try {
      // 1. 序列化值(处理循环引用)
      let valueStr;
      try {
        valueStr = JSON.stringify({
          data: value,
          expireTime: ttl ? Date.now() + ttl : Infinity
        });
      } catch (error) {
        throw new Error(`值序列化失败:${error.message}`);
      }

      // 2. 检查容量,不足则清理
      if (!this.#isCapacityEnough(valueStr)) {
        const needFreeSize = (valueStr.length * 2) - (4 * 1024 * 1024 - this.#getUsedSize());
        this.#cleanOldItems(needFreeSize);
      }

      // 3. 存入localStorage
      localStorage.setItem(key, valueStr);
      console.log(`localStorage:存入成功,key=${key}`);
    } catch (error) {
      console.error('localStorage存入失败:', error);
      throw error;
    }
  },

  /**
   * 从localStorage获取值
   * @param {string} key - 缓存键
   * @returns {any|null} 缓存值(自动JSON.parse,过期返回null)
   */
  get(key) {
    try {
      const valueStr = localStorage.getItem(key);
      if (!valueStr) return null;

      // 解析值
      const { data, expireTime } = JSON.parse(valueStr);
      const now = Date.now();

      // 检查是否过期
      if (expireTime < now) {
        localStorage.removeItem(key);
        console.log(`localStorage:项已过期,key=${key}`);
        return null;
      }

      return data;
    } catch (error) {
      console.error('localStorage获取失败:', error);
      localStorage.removeItem(key); // 清理损坏的缓存
      return null;
    }
  },

  /**
   * 删除指定项
   * @param {string} key - 缓存键
   */
  remove(key) {
    localStorage.removeItem(key);
  },

  /**
   * 清空所有项
   */
  clear() {
    localStorage.clear();
  }
};

// --------------- 使用示例 ---------------
// 存入用户配置(设置7天有效期)
LocalStorage.set(
  `userConfig:${Date.now()}:123`, // key格式:prefix:timestamp:userId
  { theme: 'dark', fontSize: 16 },
  7 * 24 * 60 * 60 * 1000
);

// 获取用户配置
const userConfig = LocalStorage.get('userConfig:1620000000000:123');
if (userConfig) {
  console.log('用户配置:', userConfig);
}
3. IndexedDB:大容量数据的分段缓存

IndexedDB无明确容量限制,但需避免一次性存储过多数据导致磁盘占用过高,策略如下:

  • 分段存储:将大数据集(如10万条历史记录)按时间分段(如每月一段),每段单独存储;
  • 过期清理:定期删除超过保留期的分段(如只保留最近6个月数据);
  • 游标查询:避免一次性读取所有数据,用游标逐段查询,减少内存占用。

IndexedDB分段缓存示例

/**
 * IndexedDB分段缓存:存储大量历史记录
 */
class IndexedDBHistoryCache {
  constructor(dbName = 'HistoryDB', storeName = 'historySegments', maxRetentionMonths = 6) {
    this.dbName = dbName;
    this.storeName = storeName;
    this.maxRetentionMonths = maxRetentionMonths;
    this.db = null;
  }

  /**
   * 打开数据库
   * @returns {Promise<IDBDatabase>} 数据库实例
   */
  #openDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, 1);

      // 数据库升级(首次创建或版本更新)
      request.onupgradeneeded = (event) => {
        const db = event.target.result;
        // 创建存储对象(按月份分段,keyPath为"month",如"2024-05")
        if (!db.objectStoreNames.contains(this.storeName)) {
          db.createObjectStore(this.storeName, { keyPath: 'month' });
        }
      };

      request.onsuccess = (event) => {
        this.db = event.target.result;
        resolve(this.db);
      };

      request.onerror = (event) => {
        reject(new Error(`IndexedDB打开失败:${event.target.error.message}`));
      };
    });
  }

  /**
   * 生成月份分段键(如"2024-05")
   * @param {Date} [date] - 日期,默认当前日期
   * @returns {string} 分段键
   */
  #getMonthKey(date = new Date()) {
    return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
  }

  /**
   * 清理过期分段(保留最近N个月)
   */
  async #cleanExpiredSegments() {
    const db = await this.#openDB();
    const transaction = db.transaction(this.storeName, 'readwrite');
    const store = transaction.objectStore(this.storeName);

    // 1. 获取所有分段键
    const segments = await new Promise((resolve, reject) => {
      const request = store.getAllKeys();
      request.onsuccess = (e) => resolve(e.target.result);
      request.onerror = (e) => reject(e.target.error);
    });

    // 2. 计算过期月份(当前月份 - maxRetentionMonths)
    const now = new Date();
    const expireMonth = new Date(now.setMonth(now.getMonth() - this.maxRetentionMonths));
    const expireMonthKey = this.#getMonthKey(expireMonth);

    // 3. 删除过期分段
    for (const monthKey of segments) {
      if (monthKey < expireMonthKey) {
        await new Promise((resolve, reject) => {
          const request = store.delete(monthKey);
          request.onsuccess = () => {
            console.log(`IndexedDB:删除过期分段,month=${monthKey}`);
            resolve();
          };
          request.onerror = (e) => reject(e.target.error);
        });
      }
    }

    transaction.oncomplete = () => db.close();
  }

  /**
   * 存入历史记录(自动按月份分段)
   * @param {Array<object>} records - 历史记录数组
   */
  async addRecords(records) {
    if (!records.length) return;

    const db = await this.#openDB();
    const transaction = db.transaction(this.storeName, 'readwrite');
    const store = transaction.objectStore(this.storeName);

    // 1. 按月份分组记录
    const recordsByMonth = {};
    records.forEach(record => {
      const recordDate = new Date(record.timestamp);
      const monthKey = this.#getMonthKey(recordDate);
      if (!recordsByMonth[monthKey]) {
        recordsByMonth[monthKey] = [];
      }
      recordsByMonth[monthKey].push(record);
    });

    // 2. 存入每个月份的分段
    for (const [monthKey, monthRecords] of Object.entries(recordsByMonth)) {
      // 获取现有分段(若存在则合并)
      const existingSegment = await new Promise((resolve) => {
        const request = store.get(monthKey);
        request.onsuccess = (e) => resolve(e.target.result || { month: monthKey, records: [] });
      });

      // 合并并去重(避免重复存储)
      const existingIds = new Set(existingSegment.records.map(r => r.id));
      const newRecords = monthRecords.filter(r => !existingIds.has(r.id));
      existingSegment.records.push(...newRecords);

      // 存入数据库
      await new Promise((resolve, reject) => {
        const request = store.put(existingSegment);
        request.onsuccess = () => {
          console.log(`IndexedDB:存入分段${monthKey},新增记录${newRecords.length}`);
          resolve();
        };
        request.onerror = (e) => reject(e.target.error);
      });
    }

    // 3. 存入后清理过期分段
    await this.#cleanExpiredSegments();

    transaction.oncomplete = () => db.close();
  }

  /**
   * 查询历史记录(按时间范围)
   * @param {Date} startDate - 开始日期
   * @param {Date} endDate - 结束日期
   * @returns {Promise<Array<object>>} 匹配的记录
   */
  async queryRecords(startDate, endDate) {
    const db = await this.#openDB();
    const transaction = db.transaction(this.storeName, 'readonly');
    const store = transaction.objectStore(this.storeName);

    // 1. 确定需要查询的月份分段
    const startMonthKey = this.#getMonthKey(startDate);
    const endMonthKey = this.#getMonthKey(endDate);
    const monthKeys = [];
    let currentMonth = new Date(startDate);
    while (this.#getMonthKey(currentMonth) <= endMonthKey) {
      monthKeys.push(this.#getMonthKey(currentMonth));
      currentMonth.setMonth(currentMonth.getMonth() + 1);
    }

    // 2. 查询每个分段的记录
    const allRecords = [];
    for (const monthKey of monthKeys) {
      const segment = await new Promise((resolve) => {
        const request = store.get(monthKey);
        request.onsuccess = (e) => resolve(e.target.result || { records: [] });
      });

      // 筛选时间范围内的记录
      const filtered = segment.records.filter(record => {
        const recordTime = new Date(record.timestamp);
        return recordTime >= startDate && recordTime <= endDate;
      });

      allRecords.push(...filtered);
    }

    transaction.oncomplete = () => db.close();
    return allRecords;
  }
}

// --------------- 使用示例 ---------------
const historyCache = new IndexedDBHistoryCache('OrderHistoryDB', 'orderSegments', 6);

// 存入100条订单历史(自动按月份分段)
const orders = Array(100).fill().map((_, i) => ({
  id: `order-${i}`,
  amount: Math.random() * 1000,
  timestamp: new Date(Date.now() - i * 86400000).toISOString() // 过去100天的订单
}));
historyCache.addRecords(orders);

// 查询最近30天的订单
const startDate = new Date();
startDate.setDate(startDate.getDate() - 30);
const recentOrders = await historyCache.queryRecords(startDate, new Date());
console.log('最近30天订单数量:', recentOrders.length);

5.3.2.3 动态容量调整:根据设备与网络适配

缓存容量并非固定不变,需根据设备性能(内存、磁盘)和网络状况动态调整:

  • 设备性能检测:用navigator.deviceMemory(检测设备内存,如4GB/8GB)、navigator.hardwareConcurrency(CPU核心数)调整缓存容量——低端设备(<4GB内存)缓存容量减半;
  • 网络状况适配:用navigator.connection.effectiveType(检测网络类型,如2G/4G/Wi-Fi)调整缓存策略——弱网环境(2G/3G)增大缓存容量,减少重复请求;Wi-Fi环境可适当减小缓存,优先获取最新数据。

动态容量调整实现

/**
 * 动态缓存容量计算器:根据设备和网络调整容量
 */
const DynamicCacheCapacity = {
  /**
   * 检测设备内存(GB)
   * @returns {number} 设备内存(如4、8)
   */
  #getDeviceMemory() {
    // navigator.deviceMemory返回值:0.25, 0.5, 1, 2, 4, 8(GB)
    return navigator.deviceMemory || 4; // 默认4GB
  },

  /**
   * 检测网络类型
   * @returns {string} 网络类型(2g/3g/4g/wifi/unknown)
   */
  #getNetworkType() {
    if (!navigator.connection) return 'unknown';
    const { effectiveType } = navigator.connection;
    if (effectiveType.includes('2g')) return '2g';
    if (effectiveType.includes('3g')) return '3g';
    if (effectiveType.includes('4g')) return '4g';
    return 'wifi'; // 包括5g、wifi等
  },

  /**
   * 计算内存缓存最大容量(项数)
   * @param {string} cacheType - 缓存类型(api/filter/render)
   * @returns {number} 动态容量
   */
  calculateMemoryCacheSize(cacheType) {
    const deviceMemory = this.#getDeviceMemory();
    const networkType = this.#getNetworkType();

    // 基础容量(按缓存类型)
    const baseSizes = {
      api: 20,    // 接口响应缓存基础容量20项
      filter: 10, // 过滤结果缓存基础容量10项
      render: 15  // 渲染结果缓存基础容量15项
    };
    let baseSize = baseSizes[cacheType] || 10;

    // 设备内存调整:内存越大,容量越大
    const memoryMultiplier = deviceMemory >= 8 ? 1.5 : (deviceMemory >= 4 ? 1 : 0.5);

    // 网络调整:弱网环境增大容量
    const networkMultiplier = networkType === '2g' || networkType === '3g' ? 1.2 : 1;

    // 计算最终容量(取整)
    const finalSize = Math.round(baseSize * memoryMultiplier * networkMultiplier);
    console.log(`动态缓存容量:${cacheType}缓存,设备内存${deviceMemory}GB,网络${networkType},容量=${finalSize}`);
    return finalSize;
  },

  /**
   * 计算localStorage最大容量(MB)
   * @returns {number} 最大容量(MB)
   */
  calculateLocalStorageSize() {
    const deviceMemory = this.#getDeviceMemory();
    // 内存越小,localStorage容量限制越严格(避免占用过多磁盘)
    return deviceMemory >= 4 ? 4 : 2; // 4GB+设备4MB,否则2MB
  }
};

// --------------- 使用示例 ---------------
// 1. 动态计算API缓存容量
const apiCacheSize = DynamicCacheCapacity.calculateMemoryCacheSize('api');
const dynamicApiCache = new LRUCache(apiCacheSize);

// 2. 动态设置localStorage容量限制
const maxLocalStorageMB = DynamicCacheCapacity.calculateLocalStorageSize();
// 在LocalStorage封装中使用该容量...

5.3.3 缓存有效期管理:避免数据过期与脏数据

缓存有效期管理的核心目标是「在保证数据新鲜度的前提下,最大化缓存收益」,常见策略包括「TTL机制」「定时清理」「主动失效」三类。

5.3.3.1 TTL(Time-To-Live):设置缓存过期时间

TTL是最基础的有效期管理策略,为每个缓存项设置「存活时间」,过期后自动失效。前端缓存中,TTL的实现需结合缓存类型:

  • 内存缓存:在缓存项中存储expireTime,获取时检查是否过期;
  • localStorage/sessionStorage:将expireTime与数据一起序列化存储,获取时解析并校验;
  • IndexedDB:为存储对象添加expireTime字段,查询时过滤过期项。

通用TTL缓存类实现

/**
 * 带TTL的通用缓存类(支持内存、localStorage、IndexedDB)
 * @template K - 缓存键类型
 * @template V - 缓存值类型
 */
class TTLCache {
  /**
   * 初始化TTL缓存
   * @param {Object} options - 配置
   * @param {number} options.defaultTTL - 默认有效期(毫秒,默认5分钟)
   * @param {string} options.storageType - 存储类型(memory/localStorage/indexedDB)
   * @param {number} options.maxSize - 最大容量(仅内存缓存)
   */
  constructor({
    defaultTTL = 5 * 60 * 1000,
    storageType = 'memory',
    maxSize = 100
  }) {
    this.defaultTTL = defaultTTL;
    this.storageType = storageType;
    this.maxSize = maxSize;

    // 初始化对应存储类型的缓存
    this.cache = this.#initStorage();
  }

  /**
   * 初始化存储类型
   * @returns {Object} 存储实例(含get/set/delete/clear方法)
   */
  #initStorage() {
    switch (this.storageType) {
      case 'memory':
        // 基于LRUCache扩展,添加TTL支持(已在LRUCache中实现)
        return new LRUCache(this.maxSize);
      
      case 'localStorage':
        // 复用带容量控制的LocalStorage封装
        return LocalStorage;
      
      case 'indexedDB':
        // 简化版IndexedDB TTL存储
        return {
          async get(key) {
            const db = await indexedDB.open('TTLCacheDB', 1);
            const transaction = db.transaction('ttlStore', 'readonly');
            const store = transaction.objectStore('ttlStore');
            
            const result = await new Promise((resolve) => {
              const request = store.get(key);
              request.onsuccess = (e) => {
                const entry = e.target.result;
                if (!entry) return resolve(null);
                
                // 检查过期
                if (entry.expireTime < Date.now()) {
                  store.delete(key);
                  resolve(null);
                } else {
                  resolve(entry.value);
                }
              };
            });
            
            db.close();
            return result;
          },
          
          async set(key, value, ttl) {
            const db = await indexedDB.open('TTLCacheDB', 1);
            db.onupgradeneeded = (e) => {
              const db = e.target.result;
              if (!db.objectStoreNames.contains('ttlStore')) {
                db.createObjectStore('ttlStore', { keyPath: 'key' });
              }
            };
            
            const transaction = db.transaction('ttlStore', 'readwrite');
            const store = transaction.objectStore('ttlStore');
            
            await new Promise((resolve) => {
              store.put({
                key,
                value,
                expireTime: ttl ? Date.now() + ttl : Infinity,
                createTime: Date.now()
              });
              resolve();
            });
            
            db.close();
          },
          
          async delete(key) {
            const db = await indexedDB.open('TTLCacheDB', 1);
            const transaction = db.transaction('ttlStore', 'readwrite');
            const store = transaction.objectStore('ttlStore');
            
            await new Promise((resolve) => {
              store.delete(key);
              resolve();
            });
            
            db.close();
          },
          
          async clear() {
            const db = await indexedDB.open('TTLCacheDB', 1);
            const transaction = db.transaction('ttlStore', 'readwrite');
            const store = transaction.objectStore('ttlStore');
            
            await new Promise((resolve) => {
              store.clear();
              resolve();
            });
            
            db.close();
          }
        };
      
      default:
        throw new Error(`不支持的存储类型:${this.storageType}`);
    }
  }

  /**
   * 存入缓存(自动应用默认TTL)
   * @param {K} key - 缓存键
   * @param {V} value - 缓存值
   * @param {number} [ttl] - 自定义有效期(毫秒,覆盖默认)
   */
  async set(key, value, ttl = this.defaultTTL) {
    await this.cache.set(key, value, ttl);
  }

  /**
   * 获取缓存(自动过滤过期项)
   * @param {K} key - 缓存键
   * @returns {Promise<V|null>} 缓存值
   */
  async get(key) {
    return this.cache.get(key);
  }

  /**
   * 删除指定缓存项
   * @param {K} key - 缓存键
   */
  async delete(key) {
    await this.cache.delete(key);
  }

  /**
   * 清空所有缓存
   */
  async clear() {
    await this.cache.clear();
  }

  /**
   * 批量设置缓存
   * @param {Array<{ key: K, value: V, ttl?: number }>} items - 缓存项数组
   */
  async batchSet(items) {
    for (const item of items) {
      await this.set(item.key, item.value, item.ttl);
    }
  }
}

// --------------- 使用示例 ---------------
// 1. 初始化内存TTL缓存(默认5分钟有效期,容量20)
const memoryTTLCache = new TTLCache({
  defaultTTL: 5 * 60 * 1000,
  storageType: 'memory',
  maxSize: 20
});

// 存入接口响应(自定义10分钟有效期)
await memoryTTLCache.set('api:products', [{ id: 1, name: '无线耳机' }], 10 * 60 * 1000);

// 2. 初始化localStorage TTL缓存(默认1小时有效期)
const localStorageTTLCache = new TTLCache({
  defaultTTL: 60 * 60 * 1000,
  storageType: 'localStorage'
});

// 存入用户偏好(永久有效,TTL设为Infinity)
await localStorageTTLCache.set('user:preference', { theme: 'dark' }, Infinity);

5.3.3.2 定时清理:主动淘汰过期缓存

TTL机制依赖「获取时检查」,若缓存项长期不被访问,过期后仍会占用内存/存储,需通过「定时清理」主动淘汰过期项:

  • 内存缓存:用setInterval定期遍历缓存,删除过期项;
  • localStorage/IndexedDB:结合页面加载、用户交互等时机,触发清理逻辑;
  • 注意:定时清理间隔需合理(如5-10分钟一次),避免频繁遍历影响性能。

定时清理工具实现

/**
 * 缓存定时清理工具:主动淘汰过期项
 */
class CacheCleaner {
  /**
   * 初始化清理工具
   * @param {Object} options - 配置
   * @param {number} options.cleanInterval - 清理间隔(毫秒,默认5分钟)
   * @param {Array<Object>} options.caches - 待清理的缓存实例(需含getSize/clear/遍历方法)
   */
  constructor({
    cleanInterval = 5 * 60 * 1000,
    caches = []
  }) {
    this.cleanInterval = cleanInterval;
    this.caches = caches;
    this.cleanTimer = null;
  }

  /**
   * 启动定时清理
   */
  start() {
    if (this.cleanTimer) return;

    // 立即执行一次清理
    this.#cleanExpiredItems();

    // 设置定时清理
    this.cleanTimer = setInterval(() => {
      this.#cleanExpiredItems();
    }, this.cleanInterval);

    console.log('缓存定时清理已启动,间隔:', this.cleanInterval / 1000, '秒');
  }

  /**
   * 停止定时清理
   */
  stop() {
    if (this.cleanTimer) {
      clearInterval(this.cleanTimer);
      this.cleanTimer = null;
      console.log('缓存定时清理已停止');
    }
  }

  /**
   * 清理所有缓存中的过期项
   */
  async #cleanExpiredItems() {
    console.log('开始清理过期缓存项...');
    let totalCleaned = 0;

    for (const cache of this.caches) {
      const cacheType = cache.constructor.name;
      const beforeSize = await this.#getCacheSize(cache);

      // 根据缓存类型执行清理
      if (cacheType === 'LRUCache' || cacheType === 'LFUCache') {
        // 内存缓存:遍历所有键,触发get时自动清理过期项
        const keys = Array.from(cache.cache.keys());
        for (const key of keys) {
          await cache.get(key); // get时自动删除过期项
        }
      } else if (cacheType === 'TTLCache') {
        // TTL缓存:若为IndexedDB,需查询所有项并过滤
        if (cache.storageType === 'indexedDB') {
          const db = await indexedDB.open('TTLCacheDB', 1);
          const transaction = db.transaction('ttlStore', 'readwrite');
          const store = transaction.objectStore('ttlStore');
          
          const allItems = await new Promise((resolve) => {
            const request = store.getAll();
            request.onsuccess = (e) => resolve(e.target.result);
          });
          
          // 删除过期项
          let cleaned = 0;
          for (const item of allItems) {
            if (item.expireTime < Date.now()) {
              await new Promise((resolve) => {
                store.delete(item.key);
                resolve();
              });
              cleaned++;
            }
          }
          
          totalCleaned += cleaned;
          db.close();
        }
      }

      const afterSize = await this.#getCacheSize(cache);
      const cleanedCount = beforeSize - afterSize;
      totalCleaned += cleanedCount;
      console.log(`${cacheType}${cache.storageType || 'memory'}):清理过期项${cleanedCount}`);
    }

    console.log(`缓存清理完成,共清理过期项${totalCleaned}`);
  }

  /**
   * 获取缓存当前大小
   * @param {Object} cache - 缓存实例
   * @returns {Promise<number>} 缓存大小
   */
  async #getCacheSize(cache) {
    if (cache.getSize) return cache.getSize();
    if (cache.storageType === 'localStorage') {
      return localStorage.length;
    }
    if (cache.storageType === 'indexedDB') {
      const db = await indexedDB.open('TTLCacheDB', 1);
      const transaction = db.transaction('ttlStore', 'readonly');
      const store = transaction.objectStore('ttlStore');
      
      const count = await new Promise((resolve) => {
        const request = store.count();
        request.onsuccess = (e) => resolve(e.target.result);
      });
      
      db.close();
      return count;
    }
    return 0;
  }
}

// --------------- 使用示例 ---------------
// 1. 初始化缓存实例
const apiCache = new LRUCache(20);
const userCache = new TTLCache({ storageType: 'localStorage' });

// 2. 初始化清理工具(清理间隔10分钟)
const cacheCleaner = new CacheCleaner({
  cleanInterval: 10 * 60 * 1000,
  caches: [apiCache, userCache]
});

// 3. 启动清理(页面加载时启动)
window.addEventListener('DOMContentLoaded', () => {
  cacheCleaner.start();
});

// 4. 页面卸载时停止清理
window.addEventListener('beforeunload', () => {
  cacheCleaner.stop();
});

5.3.3.3 主动失效:数据更新时同步清理缓存

TTL和定时清理属于「被动失效」,若数据在有效期内更新(如用户修改昵称),需「主动失效」相关缓存,避免旧数据干扰。主动失效的核心是「数据更新→缓存清理」的联动,常见场景:

  • 接口数据更新:调用POST/PUT/DELETE接口后,主动删除对应GET接口的缓存;
  • 用户操作触发:用户修改配置、删除数据后,主动清理相关缓存项;
  • 批量失效:按缓存键前缀清理(如删除所有api:products:*前缀的缓存)。

主动失效工具实现

/**
 * 缓存主动失效工具:数据更新时同步清理缓存
 */
class CacheInvalidator {
  /**
   * 初始化失效工具
   * @param {Array<Object>} caches - 待失效的缓存实例
   */
  constructor(caches = []) {
    this.caches = caches;
  }

  /**
   * 按键精确失效缓存
   * @param {string} key - 缓存键
   */
  async invalidateByKey(key) {
    for (const cache of this.caches) {
      await cache.delete(key);
      console.log(`缓存失效:${cache.constructor.name},key=${key}`);
    }
  }

  /**
   * 按前缀批量失效缓存
   * @param {string} prefix - 缓存键前缀(如"api:products:")
   */
  async invalidateByPrefix(prefix) {
    for (const cache of this.caches) {
      const cacheType = cache.constructor.name;
      
      if (cacheType === 'LRUCache' || cacheType === 'LFUCache') {
        // 内存缓存:遍历键,匹配前缀则删除
        const keys = Array.from(cache.cache.keys());
        for (const key of keys) {
          if (String(key).startsWith(prefix)) {
            await cache.delete(key);
            console.log(`缓存失效:${cacheType},key=${key}`);
          }
        }
      } else if (cacheType === 'TTLCache') {
        // TTL缓存:根据存储类型处理
        if (cache.storageType === 'localStorage') {
          // localStorage:遍历所有键,匹配前缀则删除
          const keys = Object.keys(localStorage);
          for (const key of keys) {
            if (key.startsWith(prefix)) {
              await cache.delete(key);
              console.log(`缓存失效:localStorage,key=${key}`);
            }
          }
        } else if (cache.storageType === 'indexedDB') {
          // IndexedDB:查询并删除匹配前缀的项
          const db = await indexedDB.open('TTLCacheDB', 1);
          const transaction = db.transaction('ttlStore', 'readwrite');
          const store = transaction.objectStore('ttlStore');
          
          const allItems = await new Promise((resolve) => {
            const request = store.getAll();
            request.onsuccess = (e) => resolve(e.target.result);
          });
          
          for (const item of allItems) {
            if (String(item.key).startsWith(prefix)) {
              await new Promise((resolve) => {
                store.delete(item.key);
                resolve();
              });
              console.log(`缓存失效:IndexedDB,key=${item.key}`);
            }
          }
          
          db.close();
        }
      }
    }
  }

  /**
   * 按正则表达式失效缓存
   * @param {RegExp} regex - 匹配缓存键的正则
   */
  async invalidateByRegex(regex) {
    for (const cache of this.caches) {
      const cacheType = cache.constructor.name;
      let keys = [];

      if (cacheType === 'LRUCache' || cacheType === 'LFUCache') {
        keys = Array.from(cache.cache.keys());
      } else if (cache.storageType === 'localStorage') {
        keys = Object.keys(localStorage);
      } else if (cache.storageType === 'indexedDB') {
        const db = await indexedDB.open('TTLCacheDB', 1);
        const transaction = db.transaction('ttlStore', 'readonly');
        const store = transaction.objectStore('ttlStore');
        const allItems = await new Promise((resolve) => {
          const request = store.getAllKeys();
          request.onsuccess = (e) => resolve(e.target.result);
        });
        keys = allItems;
        db.close();
      }

      // 匹配正则并删除
      for (const key of keys) {
        if (regex.test(String(key))) {
          await cache.delete(key);
          console.log(`缓存失效:${cacheType},key=${key}`);
        }
      }
    }
  }
}

// --------------- 使用示例 ---------------
// 1. 初始化缓存和失效工具
const apiCache = new LRUCache(20);
const userCache = new TTLCache({ storageType: 'localStorage' });
const cacheInvalidator = new CacheInvalidator([apiCache, userCache]);

// 2. 场景1:更新商品后,失效商品列表缓存
async function updateProduct(productId, data) {
  // 调用更新接口
  const response = await fetch(`/api/products/${productId}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data)
  });
  const updatedProduct = await response.json();

  // 主动失效相关缓存:精确失效单个商品,批量失效列表
  await cacheInvalidator.invalidateByKey(`api:product:${productId}`);
  await cacheInvalidator.invalidateByPrefix('api:products:page');

  return updatedProduct;
}

// 3. 场景2:用户登出后,失效所有用户相关缓存
async function logout() {
  await fetch('/api/logout', { method: 'POST' });
  // 按正则失效所有"user:"前缀的缓存
  await cacheInvalidator.invalidateByRegex(/^user:/);
}

5.3.4 实践反例:缓存策略失控的常见错误

反例类型问题分析修复方案
1. 无限缓存无容量限制1. 无容量限制,持续存储接口响应,内存占用暴涨;
2. 无有效期,数据过期后仍返回旧数据。
1. 改用LRUCache,设置容量(如20项);
2. 添加TTL(如5分钟),过期自动失效。
2. 不设有效期导致数据过期1. 用户信息更新后(如修改昵称),缓存未同步更新,显示旧数据;
2. 长期不清理,占用localStorage容量。
1. 存储时添加过期时间(如7天);
2. 获取时检查过期,过期自动删除;
3. 用户信息更新后主动失效缓存。
3. 滥用localStorage存大对象1. localStorage读写性能有限(大对象序列化/反序列化耗时);
2. 1MB数据易导致localStorage容量满,影响其他功能。
1. 改用IndexedDB存储大对象;
2. 按月份分段存储,定期清理过期数据;
3. 读取时用游标逐段查询,减少内存占用。
4. 缓存冗余重复存储1. 组件内缓存与全局缓存重复存储,内存冗余;
2. 组件卸载后,全局缓存仍持有数据,导致内存泄漏。
1. 统一缓存策略:组件内用useMemo,全局用LRUCache,避免重复;
2. 组件卸载时清理全局缓存中该组件的缓存项;
3. 全局缓存设置容量限制,自动淘汰冗余项。
5. 定时清理过于频繁1. 每秒遍历所有缓存项,主线程频繁阻塞;
2. 高频清理导致缓存命中率骤降,重复请求增多。
1. 延长清理间隔(如5-10分钟一次);
2. 结合「获取时检查过期」+「定时清理」,减少遍历频率;
3. 清理时用requestAnimationFrame,避免阻塞主线程。

5.3.5 代码评审要点:缓存策略的检查清单

评审维度检查要点工具支持
1. 缓存选型1. 是否根据数据特性(频率、大小、生命周期)选择合适的缓存类型?
2. 高频临时数据是否用内存缓存?
3. 大对象(>100KB)是否避免用localStorage?
1. 代码审查(检查缓存类型与数据特性匹配度);
2. Chrome DevTools → Application(查看缓存使用)。
2. 缓存大小控制1. 是否设置缓存容量限制(如LRU的maxSize)?
2. 容量是否合理(不超过内存/存储的安全范围)?
3. 大对象是否分段存储(如IndexedDB按月份)?
1. 检查缓存类初始化参数(如new LRUCache(20));
2. Memory面板(监控内存缓存大小);
3. Application面板(查看localStorage/IndexedDB占用)。
3. 有效期管理1. 是否为所有缓存项设置TTL?
2. TTL是否合理(高频数据短,低频数据长)?
3. 是否有定时清理或主动失效机制?
1. 检查缓存set方法是否传入ttl参数;
2. 代码审查(TTL与数据更新频率匹配度);
3. 检查是否有CacheCleaner或CacheInvalidator实例。
4. 性能影响1. 缓存读写是否阻塞主线程(如大对象JSON.stringify)?
2. 定时清理间隔是否合理(不小于5分钟)?
3. 缓存命中率是否达标(如>70%)?
1. Performance面板(查看缓存操作耗时);
2. 代码审查(清理间隔设置);
3. 埋点统计(缓存命中次数/总请求次数)。
5. 数据一致性1. 数据更新后是否主动失效相关缓存?
2. 缓存失效是否覆盖所有相关键(精确+批量)?
3. 是否有缓存降级方案(如缓存失效时直接请求)?
1. 检查数据更新接口(POST/PUT/DELETE)后是否调用invalidate方法;
2. 代码审查(失效键的完整性);
3. 检查缓存get失败时是否有降级逻辑。
6. 可维护性1. 缓存逻辑是否封装(如TTLCache类),避免散落在业务代码中?
2. 是否有缓存相关文档(容量、TTL、失效规则)?
3. 是否有缓存监控(大小、命中率、失效次数)?
1. 代码审查(缓存逻辑封装度);
2. 检查项目文档(是否包含缓存策略说明);
3. 检查是否有埋点或监控工具(如Sentry缓存监控)。

对话小剧场:电商项目缓存问题的团队攻坚

场景:电商后台「商品管理」页面上线后,测试反馈两个问题:1)频繁操作后页面卡顿,内存从200MB涨到800MB;2)商品价格修改后,列表仍显示旧价格。团队(小美、小迪、小稳、大熊、小燕)召开优化会议。

小燕(质量工程师):“根据测试数据,商品列表页面每刷新10次,内存增加100MB;修改商品价格后,刷新列表仍显示旧价格——即使强制刷新页面,有时候也需要等几分钟才更新。我用Chrome DevTools看了,localStorage里的‘productList:page1’数据已经存了3天,还没过期。”

小迪(前端开发):“我查了商品列表的缓存代码,问题出在全局缓存对象上!之前为了减少接口请求,用了一个无限制的globalCache,每次刷新列表都往里面塞数据,既没容量控制也没过期时间。”

小迪投屏展示问题代码:

// 问题代码:全局缓存无容量限制、无TTL
const globalCache = {}; // 无限存储
const localStorageCache = {
  set(key, value) {
    // 错误:不设有效期,永久存储
    localStorage.setItem(key, JSON.stringify(value));
  },
  get(key) {
    const value = localStorage.getItem(key);
    return value ? JSON.parse(value) : null;
  }
};

// 商品列表请求逻辑
async function getProductList(page = 1) {
  const cacheKey = `productList:page${page}`;
  
  // 先查内存缓存,再查localStorage,最后请求接口
  if (globalCache[cacheKey]) return globalCache[cacheKey];
  const localValue = localStorageCache.get(cacheKey);
  if (localValue) {
    globalCache[cacheKey] = localValue;
    return localValue;
  }
  
  // 请求接口后存入两个缓存
  const response = await fetch(`/api/products?page=${page}`);
  const data = await response.json();
  globalCache[cacheKey] = data; // 内存缓存无限存
  localStorageCache.set(cacheKey, data); // localStorage永久存
  return data;
}

小美(前端开发):“价格修改后显示旧数据的问题,我也找到了——修改价格的接口调用后,没清理对应的缓存!比如改了商品ID=1001的价格,productList:page1product:1001这两个缓存还在,列表刷新时优先读缓存,自然看不到新价格。”

大熊(后端开发):“后端这边也有优化空间——目前接口没返回数据更新时间戳,前端没法判断缓存是否新鲜。我可以在所有商品接口的响应头加Last-Modified,或者在返回体里加updateTime,方便前端做缓存校验。”

小稳(前端架构师):“现在问题很明确了,三个核心漏洞:1)内存缓存无容量限制,导致内存泄漏;2)无TTL和定时清理,数据永久过期;3)数据更新后无主动失效,缓存不同步。解决方案分三步:”

小稳边画架构图边说:
“第一步,统一缓存工具:把散落在业务里的缓存逻辑,换成我们之前封装的TTLCache,内存缓存用LRU控制容量(比如20项),TTL设5分钟;localStorage缓存也加TTL,容量控制在4MB以内。
第二步,补全主动失效:修改商品价格、库存后,不仅要调用后端接口,还要主动清理对应的列表缓存(如productList:page1)和单个商品缓存(如product:1001)。
第三步,后端配合:用大熊说的Last-Modified头做缓存校验,即使前端缓存没失效,也能对比时间戳,拉取最新数据。”

小迪(前端开发):“我来改缓存逻辑!把globalCache换成TTLCache,再在价格修改接口里加主动失效。”

小迪快速写出修复代码:

// 修复1:用TTLCache统一管理缓存
const productCache = new TTLCache({
  defaultTTL: 5 * 60 * 1000, // 5分钟有效期
  storageType: 'memory',
  maxSize: 20 // 内存缓存最多20项
});

// 修复2:商品列表请求(加时间戳校验)
async function getProductList(page = 1) {
  const cacheKey = `productList:page${page}`;
  const cachedData = await productCache.get(cacheKey);
  
  // 后端返回Last-Modified,对比缓存时间戳
  const response = await fetch(`/api/products?page=${page}`, {
    headers: cachedData ? { 'If-Modified-Since': cachedData.updateTime } : {}
  });
  
  if (response.status === 304) {
    // 数据未更新,返回缓存
    return cachedData;
  }
  
  const data = await response.json();
  // 存入缓存(带后端返回的updateTime)
  await productCache.set(cacheKey, { ...data, updateTime: response.headers.get('Last-Modified') });
  return data;
}

// 修复3:修改价格后主动失效缓存
async function updateProductPrice(productId, newPrice) {
  const response = await fetch(`/api/products/${productId}/price`, {
    method: 'PUT',
    body: JSON.stringify({ price: newPrice })
  });
  
  if (response.ok) {
    // 主动失效单个商品和列表缓存
    await productCache.delete(`product:${productId}`);
    await cacheInvalidator.invalidateByPrefix('productList:page');
  }
  
  return response.json();
}

小美(前端开发):“我再补个定时清理!页面初始化时启动CacheCleaner,每10分钟清理一次过期缓存,避免内存缓存长期占用。”

// 页面初始化时启动清理
window.addEventListener('DOMContentLoaded', () => {
  const cleaner = new CacheCleaner({
    cleanInterval: 10 * 60 * 1000,
    caches: [productCache]
  });
  cleaner.start();
});

小燕(质量工程师):“我马上测!修改后刷新10次列表,内存稳定在220MB左右,没再增长;修改商品价格后,列表立即显示新价格,localStorage里的旧缓存也被清理了。后端加了Last-Modified后,304响应率从0涨到60%,重复请求少了很多!”

大熊(后端开发):“后续我们还可以加个‘缓存刷新接口’,比如大促前批量更新商品时,后端主动通知前端清理缓存,不用等TTL过期。”

小稳(前端架构师):“最后补充个规范——以后所有缓存都要走统一的TTLCacheCacheInvalidator,禁止在业务代码里写散的globalCachelocalStorage操作。代码评审时重点查三个点:容量限制、TTL、主动失效,避免再踩同样的坑。”

5.3.6 小结:缓存策略的“收益-成本”平衡法则

前端缓存的核心不是“越多越好”,而是在“性能收益”与“内存/存储成本”之间找到平衡。通过本章的分析,可总结出三类关键实践:

  1. 大小控制:用LRU/LFU算法限制缓存容量,结合场景分配资源(内存缓存<页面内存20%,localStorage<4MB),动态适配设备与网络;
  2. 有效期管理:为所有缓存项设置TTL(高频数据短、低频数据长),配合定时清理主动淘汰过期项,数据更新时通过主动失效保证一致性;
  3. 工具化封装:将缓存逻辑封装为TTLCacheCacheCleaner等可复用工具,避免散落在业务代码中,同时通过代码评审和监控(如缓存命中率、内存占用)持续优化。

合理的缓存策略能将重复请求减少70%以上,页面加载时间缩短50%,但失控的缓存会沦为“内存黑洞”。记住:好的缓存不仅要“存得快、取得快”,更要“控得住、清得掉”——这才是前端内存优化中缓存的真正价值。

下一节将探讨“内存优化的进阶技巧”,包括弱引用的深度应用、内存碎片整理等高级话题,帮助应对更复杂的内存管理场景。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值