R34第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

记忆化的实际价值

记忆化的实际价值 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

这一课回答一个常见问题:什么时候值得加缓存。读完你应该会用计时工具测量一段计算的真实开销,能解释 useMemo 的依赖数组为什么就是缓存的「失效条件」,并理解缓存自带的维护成本——从而在「缓存」和「不缓存」之间给出有依据的选择,而不是见到计算就包一层 useMemo。

问题场景

课程库页面要按关键词对 5000 门课做模糊匹配加排序,列表滚动有点卡。同事建议「把所有计算都加 useMemo,React 就是这么优化的」。第一版改完,文件里冒出十几个 useMemo,卡不卡没再量过;新同事改这段代码时,还得小心翼翼维护每个依赖数组。

缓存不是免费的:它用「记录依赖、每次渲染比较依赖、查找缓存」换来「跳过重算」。如果计算本身只要 0.1ms,这层包装反而更亏。

核心解释

判断标准只有一个:先测量,再决定。最粗糙的手段也够用——把怀疑的代码前后各记一次时间:

console.time("filter");
const result = courses.filter((c) => match(c, keyword)).sort(byRating);
console.timeEnd("filter"); // 打印这段计算的真实耗时

经验参考:单次执行低于 1ms 的计算,加 useMemo 几乎测不出差别,还引入依赖维护负担;10ms 以上、且在高频操作(每次按键、每次滚动)中反复执行的计算,缓存才真正赚钱。

第二件事是理解缓存的钥匙:useMemo 只认依赖数组。依赖没变就返回旧值,依赖变了就重算——所以依赖数组就是这份缓存的失效条件,写漏一个输入,界面就会用过期数据,这比慢更难查:

const hot = useMemo(
  () => courses.filter((c) => match(c, keyword)).sort(byRating),
  [courses, keyword] // 影响结果的每个输入都必须在这里,少一个就是错的
);

维护成本同样真实:以后任何人改这段计算、多引入一个输入,都必须同步改依赖数组。所以默认不缓存,把缓存当作「测量之后对症下药」的工具——这也是 React 官方文档明确的态度。

示例拆解

实验区提供 5000 门课的模拟数据和搜索框:

  • 页面同时跑两份相同计算:「不加缓存」每次渲染都重算,「useMemo 缓存」依赖为 [keyword, amplify];
  • 两份计算各自记录累计执行次数和单次耗时,直接显示在页面上;
  • 点「无关刷新」制造一次与计算无关的重渲染:不加缓存的次数 +1,缓存版不重算——这就是缓存赚到的部分;
  • 打开「数值放大」开关(计算量 ×30)后再输入关键词,不加缓存时输入明显掉帧;
  • 验证方式:对比两种模式的累计次数与耗时,再用控制台的 console.time 亲手量一次计算耗时,判断你自己的取舍。

动手任务

打开本课实验区,完成三个小任务:

  1. 点三次「无关刷新」,记录两个累计次数的差;解释为什么缓存版的数字不动。
  2. 打开「数值放大」,快速输入几轮关键词,记录不加缓存时的单次耗时;此时你还反对缓存吗?
  3. 在控制台用 console.time / console.timeEnd 包住一次搜索计算,记下数字,和页面上显示的耗时对照。

常见错误

  • 无差别包 useMemo:计算只要 0.05ms,缓存比较和依赖维护反而是净负担;先测量再动手。
  • 依赖数组写漏输入:结果不随关键词更新,这类「数据过期」在代码审查里很难一眼看出。
  • 在 useMemo 里放副作用:useMemo(() => { saveLog(); return calc(); }, ...)——缓存可能被跳过也可能重跑,副作用执行时机不可控;副作用应放在事件处理或 Effect 里。
  • 把 useMemo 当正确性保证:React 不承诺缓存永不失效,未来版本可能丢弃缓存;正确性永远不能依赖缓存存在。

检查与复盘

自查清单:

  • 能复述「先测量再缓存」的流程,并说出大致的判断阈值
  • 把依赖数组理解为缓存的失效条件:改计算必查依赖
  • 会用 console.time 或性能面板给一段计算计时,并保留测量数字

参考资料

动手实验
动手实验
预览
点击「运行」在隔离沙箱中预览
检查题共 2 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1单选简单

下列哪种情况不值得使用 useMemo?

Q2读代码较难

阅读代码(React 18+)。items = ['apple', 'banana', 'cherry'],由父组件以稳定引用传入。初始显示全部三项。点击「筛选 a」按钮后列表会怎样?再点「刷新」按钮呢?

function List({ items }) {
  const [filterText, setFilterText] = useState('');
  const [refresh, setRefresh] = useState(0);

  const visible = useMemo(() => {
    return items.filter((i) => i.includes(filterText));
  }, [items]); // 注意:漏写了 filterText

  return (
    <>
      <ul>
        {visible.map((i) => (
          <li key={i}>{i}</li>
        ))}
      </ul>
      <button onClick={() => setFilterText('a')}>筛选 a</button>
      <button onClick={() => setRefresh(refresh + 1)}>刷新</button>
    </>
  );
}

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...