一句话定义
三个 API 共享同一思想——缓存:useMemo 缓存计算结果、useCallback 缓存函数引用、memo 缓存组件渲染结果;触发缓存失效的唯一开关是依赖数组。
为什么重要
它们是 React 面试与代码评审争议的旋涡中心:有人到处包、有人一律不用。正确姿势来自对成本模型的理解——缓存本身有成本(内存、依赖数组维护、引用比较),所以优化的前提永远是「先测量」(kp-019),React Compiler 的出现又改变了这条权衡线(见本篇末节)。
前置知识
- kp-008 渲染模型与「父渲染 → 子渲染」的默认行为。
- 引用相等性(
{} !== {})。
核心概念
- useMemo(fn, deps):deps 不变 → 返回上次 fn 的结果,跳过 fn 执行。
- useCallback(fn, deps):等价于
useMemo(() => fn, deps);deps 不变 → 同一个函数引用。 - memo(Component):浅比较 props,全部相等 → 跳过整个子树渲染。
- 浅比较的坑:props 里的对象/数组/函数每次渲染都是新引用 → memo 无效;这就是记忆化三件套必须配合使用的原因。
- Compiler 视角(React 19 时代):React Compiler 尝试自动记忆化,人工使用这些 API 的必要性下降,但理解原理仍必要。
原理 / 机制
三件套协同的标准场景:
jsxconst Row = memo(function Row({ item, onSelect }) {
return <li onClick={() => onSelect(item.id)}>{item.name}</li>;
});
function List({ items, query }) {
// 1. 缓存计算:昂贵过滤不随无关 state 重跑
const filtered = useMemo(
() => items.filter(i => i.name.includes(query)),
[items, query]
);
// 2. 缓存引用:不因 List 重渲染而让 Row 的 memo 失效
const handleSelect = useCallback(id => {
setSelectedId(id);
}, []); // 只用了 setter(稳定) → 空依赖安全
return (
<ul>
{filtered.map(i => <Row key={i.id} item={i} onSelect={handleSelect} />)}
</ul>
);
}
只包 memo(Row) 而不包 useCallback 会怎样:List 每次渲染创建新 handleSelect → 浅比较失败 → Row 照样重渲染 → memo 白做。三者是一条链,断一环全失效。
useMemo 的另一个独立价值——引用稳定性(不只是省计算):
jsx// kp-012 的 Context value 必须稳定,否则全体消费者陪跑
const value = useMemo(() => ({ theme, setTheme }), [theme]);
memo 的浅比较语义与豁免:
jsx// 浅比较:Object.is 逐个比 props
// ✅ 相等:数字/字符串/布尔/同一个函数引用
// ❌ 不等:任何内联对象/数组/箭头函数
// 豁免姿势:children 会绕过浅比较(父渲染 → children 新元素)
const Item = memo(function Item({ children }) { ... });
// <Item><b>x</b></Item> 中 <b>x</b> 元素每次渲染都是新对象 → memo 失效
// 修法:把内容作为字符串/数据传,或接受它总是重渲染
成本模型的量化直觉:
textuseMemo(fn): 保存结果 + 比较依赖数组 换取「跳过 fn」
memo(C): 保存上次输出 + 浅比较 props 换取「跳过 C 子树渲染」
两者收益 > 成本的条件:fn/渲染真的贵 && deps 变化频率低
直观类比
useMemo 像保险柜里的成品:钥匙没换(deps 不变)就直接开柜取货,不再进车间重做。useCallback 像专线电话号码:号码不变,对方(memo 子组件)就知道「还是同一个人打来的」,不用每次重新登记。memo 像前台闸机:工牌(props)没变就放行免检——但工牌上贴了张手写便条(内联对象/函数),每次都算新工牌,闸机形同虚设。
实例 / 案例
先测量、再优化的完整案例(学习路径第三阶段项目的目标):
jsx// Profiler 发现:滚动时 1000 行 Row 全部重渲染,主线程掉帧
// 1) 打开 React DevTools Profiler → Flamegraph 确认 Row 重渲染原因:
// "onSelect changed" / "item changed"
// 2) 若 item 是父组件 map 出来的稳定对象 → 只需 memo + useCallback
// 3) 若父组件每次渲染都重建 items 数组 → useMemo(items)
// 4) 若 query 输入导致高频过滤 5 万条 → useMemo + 虚拟化(react-window)
决策树(贴在脑子里):
text有性能问题吗?
├─ 没有 → 不用任何记忆化(默认干净代码)
└─ 有 → Profiler 定位是谁渲染慢/渲染多
├─ 子组件因「函数引用」重渲染 → useCallback + memo
├─ 子组件因「对象/数组引用」重渲染 → useMemo + memo
├─ 计算本身慢 → useMemo(或上移到事件/Worker)
└─ 列表长 → 虚拟化 > 记忆化
常见误区
- 「到处 useMemo 更安全」:依赖数组写错反而引入 stale 值 bug;缓存过期比不缓存更危险。Compiler 的动机之一就是让人少手写。
- useCallback 单独使用:没有 memo 子组件消费它,只是白存一份引用。
- 依赖数组里塞函数调用:
useMemo(() => x, [f()])依赖的是返回值而不是 f;依赖必须是数据引用。 - 把 useMemo 当「保证只执行一次」:它不是「一次」,是「deps 变就重算」;副作用依然禁止(与 kp-016 reducer 同理)。
- memo + 不稳定 children/Context:children 新元素、context 变化都会绕过 memo(见豁免姿势)。
自测题
- useCallback 的函数签名等价于哪种 useMemo 调用?两者缓存的东西有何不同?
- 为什么 memo 组件必须搭配 useCallback/useMemo 才有效?给出一个三者缺一即失效的最小反例。
- memo 组件接收 children 时为什么仍会重渲染?两种修法?
- 「有 useMemo 总比没有好」错在哪?举一个「记忆化引入 bug」的例子。
- React Compiler 的目标是什么?它上线后,人工记忆化 API 的定位变成了什么?
与其他知识点的关系
- 向前:浅比较与 kp-008 的 diff、kp-012 的 value 稳定、kp-014 的闭包引用互相咬合。
- 向后:kp-019 是「何时该用」的测量方法论主场;kp-022 的并发调度不要求记忆化(调优先级 ≠ 提速);kp-025 提及 Compiler 与 React 19 生态的关系。
延伸阅读
- useMemo / useCallback / memo API 页(官方):<https://react.dev/reference/react/useMemo>
- Memoization(官方 Learn 章节):<https://react.dev/learn/skipping-re-rendering>