React.memo & useMemo:为什么 React 里「看起来没变的组件」还是渲染了

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 产生」。

回到上面的例子:

  1. 点击按钮 → setCountParent 重新渲染。
  2. Parent 重新执行函数体 → 执行到 <Child data={data} />
  3. 由于 data = { value: 1 }字面量对象 ,每次执行都会生成新的引用
  4. React 比对 <Child> 的前后 props:新的 data !== 旧的 data(引用不同)。
  5. 于是 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. 什么时候「可以不用」这对组合拳

  1. 组件渲染开销极低(渲染一次 < 1ms)。
  2. props 本身就是稳定的原始值
  3. 组件层级很浅,重新渲染不会波及大量子树。
  4. 一次性页面 / 不常交互的界面
  5. 团队约定:「优先写清晰代码,遇到真实性能瓶颈再回退优化」。

一句话:不要为了优化而优化

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."

相关推荐
孟陬12 分钟前
HTML 处理以及性能对比 - Bun 单元测试系列
react.js·单元测试·bun
逾明17 分钟前
Electron自定义菜单栏及Mac最大化无效的问题解决
前端·electron
辰九九21 分钟前
Uncaught URIError: URI malformed 报错如何解决?
前端·javascript·浏览器
月亮慢慢圆21 分钟前
Echarts的基本使用(待更新)
前端
芜青34 分钟前
实现文字在块元素中水平/垂直居中详解
前端·css·css3
useCallback38 分钟前
Elpis全栈项目总结
前端
小高0071 小时前
React useMemo 深度指南:原理、误区、实战与 2025 最佳实践
前端·javascript·react.js
LuckySusu1 小时前
【js篇】深入理解类数组对象及其转换为数组的多种方法
前端·javascript
LuckySusu1 小时前
【js篇】数组遍历的方法大全:前端开发中的高效迭代
前端·javascript
LuckySusu1 小时前
【js篇】for...in与 for...of 的区别:前端开发中的迭代器选择
前端·javascript