0. 前言:先给现象
下面这段代码你一定写过或见过:
jsx
function Child({ data }) {
console.log('Child render');
return <div>{data.value}</div>;
}
function Parent() {
const [count, setCount] = useState(0);
const data = { value: 1 }; // 每次都新建对象
return (
<>
<button onClick={() => setCount(c => c + 1)}>父组件 count: {count}</button>
<Child data={data} />
</>
);
}
现象 :无论 data.value
有没有变,每次点击按钮都会打印
Child render
。
灵魂拷问 :为什么?我已经没改 data.value
啊!
1. 先弄清「是谁」在触发渲染
React 的渲染流程只有一条规则:
只要父组件重新渲染,子组件就会跟着渲染一次 。
至于子组件的 props 是否「真的变了」并不是 React 关心的,它只关心「有没有新的 ReactElement 产生」。
回到上面的例子:
- 点击按钮 →
setCount
→Parent
重新渲染。 Parent
重新执行函数体 → 执行到<Child data={data} />
。- 由于
data = { value: 1 }
是字面量对象 ,每次执行都会生成新的引用。 - React 比对
<Child>
的前后 props:新的 data !== 旧的 data(引用不同)。 - 于是 Child 被迫重新渲染。
2. useMemo 解决什么问题
2.1 本质
useMemo
用「缓存」阻止「每次渲染都产生新的对象、数组、函数等引用值」。
diff
- const data = { value: 1 };
+ const data = useMemo(() => ({ value: 1 }), []);
加上后:
- 只有依赖数组
[]
内的值变化时,才会重新计算并返回新引用; - 否则始终返回同一份引用 。
对 React 来说,props 不变,子组件就可以跳过渲染。
2.2 不用 useMemo 的后果
场景 | 后果 |
---|---|
把对象/数组/函数作为 props 传给子组件 | 每次父组件渲染都会触发子组件重新渲染 |
把对象/数组/函数作为依赖传给 useEffect/useMemo/useCallback |
每次依赖都是新的引用 → 导致副作用重新执行、缓存失效 |
一句话:引用不稳定 → 连锁渲染 / 副作用反复执行。
3. React.memo 解决什么问题
3.1 本质
React.memo(Component)
给组件本身加一层浅比较:
jsx
const Child = React.memo(function Child({ data }) {
console.log('Child render');
return <div>{data.value}</div>;
});
当父组件重新渲染时,React 会先比较 prevProps === nextProps
(浅比较)。
如果 props 全部浅相等,则直接跳过该组件的渲染。
3.2 不用 React.memo 的后果
- 无论 props 是否真的变化,子组件都会随父组件一起渲染。
- 在「组件树很深 / 列表很大」时,浪费大量 CPU 时间。
4. 为什么常常需要「useMemo + React.memo」组合拳
场景 | 是否需要 useMemo | 是否需要 React.memo | 原因 |
---|---|---|---|
父组件频繁刷新,子组件 props 是原始值(number、string) | ❌ | ❌ | 原始值自然稳定,子组件自己开销不大,可忽略 |
父组件频繁刷新,子组件 props 是对象/数组/函数 | ✅ | ✅ | 用 useMemo 让引用稳定,用 React.memo 让 React 跳过渲染 |
子组件本身就是重计算/重渲染的组件 | ✅ | ✅ | 节省大量时间 |
子组件渲染代价极小(纯文本、样式简单) | ❌ | ❌ | 优化收益低于代码复杂度 |
列表渲染(map) | ✅ | ✅ | 列表元素对象必须稳定引用,否则全部子项都会重渲染 |
Context 的 Provider value | ✅ | ❌ | 保证 value 引用稳定,避免所有消费组件渲染 |
5. 什么时候「可以不用」这对组合拳
- 组件渲染开销极低(渲染一次 < 1ms)。
- props 本身就是稳定的原始值。
- 组件层级很浅,重新渲染不会波及大量子树。
- 一次性页面 / 不常交互的界面。
- 团队约定:「优先写清晰代码,遇到真实性能瓶颈再回退优化」。
一句话:不要为了优化而优化 。
React 团队给出的经验值是:
大多数交互型应用在不做任何手动优化的情况下已经足够快。
6. 常见误区澄清
误区 | 正解 |
---|---|
"用了 useMemo 就一定更快" | 如果计算本身很轻,useMemo 反而多了缓存比较的开销 |
"React.memo 是浅比较,所以深层对象必须深比较" | 99% 场景里把对象提到 useMemo 里保持引用稳定即可,不需要写自定义比较函数 |
"useCallback 能代替 useMemo" | useCallback(fn, deps) 其实就是 useMemo(() => fn, deps),二者各司其职,不要混淆 |
"React.memo 会阻止子组件内 state 变化后的重渲染" | ❌,React.memo 只跳过「父组件传递的 props 没变」的情况,子组件自身 setState 依旧会触发重渲染 |
7. 一张流程图帮你记住
父组件渲染
│
├─ props 是原始值?
│ ├─ 是 → 子组件是否 React.memo?
│ │ ├─ 是 → 跳过渲染(props 没变)
│ │ └─ 否 → 跟着渲染
│ └─ 否 → props 是对象/函数/数组?
│ ├─ 用 useMemo 保持引用稳定 → 子组件 React.memo → 跳过渲染
│ └─ 不用 useMemo → 引用必变 → 子组件必渲染
│
└─ 子组件自己 setState → 与上面所有逻辑无关 → 必然渲染
8. 结尾:一句话总结
- useMemo:让「值」的引用稳定;
- React.memo:让「组件」在 props 不变时跳过渲染;
- 何时用:当「不稳定引用」带来的连锁渲染真的产生可感知的性能问题时再出手;
- 何时不用:组件渲染开销小、代码复杂度大、团队约定先度量后优化。
记住 React 官方文档里那句老话:
"Make it work, make it right, make it fast --- in that order."