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

相关推荐
qczg_wxg2 小时前
React Native的动画系统
javascript·react native·react.js
漂流瓶jz3 小时前
解锁Babel核心功能:从转义语法到插件开发
前端·javascript·typescript
周小码4 小时前
shadcn-table:构建高性能服务端表格的终极解决方案 | 2025最新实践
前端·react.js
大怪v4 小时前
老乡,别走!Javascript隐藏功能你知道吗?
前端·javascript·代码规范
Winson℡4 小时前
在 React Native 层禁止 iOS 左滑返回(手势返回/手势退出)
react native·react.js·ios
webYin5 小时前
vue2 打包生成的js文件过大优化
前端·vue.js·webpack
gnip5 小时前
结合Worker通知应用更新
前端·javascript
叶玳言5 小时前
【LVGL】从HTML到LVGL:嵌入式UI的设计迁移与落地实践
前端·ui·html·移植
高级测试工程师欧阳5 小时前
HTML 基本结构
前端
Gazer_S5 小时前
【Element Plus 表单组件样式统一 & CSS 文字特效实现指南】
前端·css·vue.js