记忆化的实际价值
记忆化的实际价值 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
这一课回答一个常见问题:什么时候值得加缓存。读完你应该会用计时工具测量一段计算的真实开销,能解释 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 亲手量一次计算耗时,判断你自己的取舍。
动手任务
打开本课实验区,完成三个小任务:
- 点三次「无关刷新」,记录两个累计次数的差;解释为什么缓存版的数字不动。
- 打开「数值放大」,快速输入几轮关键词,记录不加缓存时的单次耗时;此时你还反对缓存吗?
- 在控制台用
console.time/console.timeEnd包住一次搜索计算,记下数字,和页面上显示的耗时对照。
常见错误
- 无差别包 useMemo:计算只要 0.05ms,缓存比较和依赖维护反而是净负担;先测量再动手。
- 依赖数组写漏输入:结果不随关键词更新,这类「数据过期」在代码审查里很难一眼看出。
- 在 useMemo 里放副作用:
useMemo(() => { saveLog(); return calc(); }, ...)——缓存可能被跳过也可能重跑,副作用执行时机不可控;副作用应放在事件处理或 Effect 里。 - 把 useMemo 当正确性保证:React 不承诺缓存永不失效,未来版本可能丢弃缓存;正确性永远不能依赖缓存存在。
检查与复盘
自查清单:
- 能复述「先测量再缓存」的流程,并说出大致的判断阈值
- 把依赖数组理解为缓存的失效条件:改计算必查依赖
- 会用 console.time 或性能面板给一段计算计时,并保留测量数字
参考资料
- React 官方文档:useMemo —— 官方明确写了「不需要就不加」,含判断昂贵计算的方法
- React 官方文档:useCallback —— 函数版缓存的对应 API,下一课会大量使用
- React 官方文档:memo —— 缓存组件本身的场景,与下一课衔接
读完这一课了?
标记完成后,工作台会安排接下来的学习。