React 原理进阶:彻底理解 re-render、React.memo、useMemo 与 useCallback

很多人写 React 时,会频繁遇到这些词:

  • re-render
  • React.memo
  • useMemo
  • useCallback

它们看起来都是"性能优化",但如果没有先理解 React 的渲染模型,很容易陷入两个误区:

text 复制代码
误区一:
组件 re-render
=
DOM 全部重建

误区二:
useMemo / useCallback / memo
=
用了就一定更快

实际上,这几个 API 都建立在同一个前提上:

React 的核心更新模型是:状态变化后重新执行组件,重新计算 UI 描述,再决定哪些 DOM 真正需要更新。

本文就从 re-render 开始,把这几个概念完整串起来。


一、React 什么时候会 re-render?

最常见的触发原因有三个:

text 复制代码
1. 当前组件自己的 state 更新

2. 父组件重新渲染

3. 当前组件消费的 Context 发生变化

例如:

tsx 复制代码
import { useState } from 'react'

function App() {
  const [count, setCount] = useState(0)

  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  )
}

点击按钮:

tsx 复制代码
setCount(count + 1)

大致会发生:

text 复制代码
state 更新
↓
App 重新执行
↓
重新计算 JSX
↓
React 比较更新前后的结果
↓
提交真正需要修改的 DOM

这里最重要的一点是:

re-render 并不等于 DOM 全部重建。


二、什么叫"组件重新渲染"?

考虑:

tsx 复制代码
function App() {
  const [count, setCount] = useState(0)

  console.log('App render')

  return (
    <div>
      <p>Hello React</p>

      <p>{count}</p>

      <button onClick={() => setCount(count + 1)}>
        +1
      </button>
    </div>
  )
}

每次点击:

text 复制代码
+1

控制台都会打印:

text 复制代码
App render

说明:

tsx 复制代码
App()

又执行了一次。

但注意:

tsx 复制代码
<p>Hello React</p>

内容根本没有变化。

React 并不意味着一定会把这段真实 DOM 删除再重新创建。

所以需要区分:

text 复制代码
Render
=
重新执行组件
重新计算 UI 描述

和:

text 复制代码
DOM Update
=
真正修改浏览器 DOM

两者不是一个概念。


三、为什么需要重新执行组件?

React 组件本质上可以粗略理解成:

text 复制代码
State
↓
Component Function
↓
JSX

例如:

tsx 复制代码
function Counter() {
  const [count, setCount] = useState(0)

  return <p>{count}</p>
}

当:

text 复制代码
count = 0

组件描述:

html 复制代码
<p>0</p>

当:

text 复制代码
count = 1

组件应该描述:

html 复制代码
<p>1</p>

所以 React 的思路是:

状态变了,那我重新执行组件,看一下新状态下 UI 应该是什么样。

然后再决定真正需要修改什么 DOM。

这是一种典型的声明式 UI 思想。


四、父组件重新渲染,子组件会发生什么?

例如:

tsx 复制代码
function Child() {
  console.log('Child render')

  return <p>Child</p>
}

function App() {
  const [count, setCount] = useState(0)

  console.log('App render')

  return (
    <>
      <button onClick={() => setCount(count + 1)}>
        {count}
      </button>

      <Child />
    </>
  )
}

即使 Child

  • 没有 state
  • 没有 props
  • 和 count 完全没关系

当 App 更新时,默认情况下 Child 也会重新执行。

大致:

text 复制代码
setCount()
↓
App render
↓
Child render

这就是 React 很重要的默认行为:

父组件重新渲染时,其子组件默认也会进入新的 render 流程。

但还是那句话:

text 复制代码
Child render
≠
Child DOM 一定发生变化

五、React.memo 是干什么的?

如果一个子组件:

text 复制代码
props 没变化

并且它自身重新执行的成本比较高,那么可以考虑:

tsx 复制代码
import { memo } from 'react'

const Child = memo(function Child() {
  console.log('Child render')

  return <p>Child</p>
})

现在父组件发生更新:

text 复制代码
App render
↓
React 检查 Child 的 props
↓
props 没变化
↓
跳过 Child 的重新 render

因此:

React.memo 是一种基于 props 比较的组件级优化。

可以先简单理解成:

text 复制代码
React.memo
=
如果 props 没变
就尝试复用上一次结果

六、React.memo 并不是"禁止渲染"

这一点很重要。

很多人会误解:

tsx 复制代码
memo(Component)

之后:

这个组件就不会重新渲染了。

不是。

如果 props 变化:

text 复制代码
old props
≠
new props

组件仍然需要重新渲染。

例如:

tsx 复制代码
<Child count={count} />

只要:

text 复制代码
count 改变

即使 Child 使用:

tsx 复制代码
memo(...)

仍然会 render。

所以 memo 不是"锁死组件"。

它只是:

在 props 没有变化时提供跳过 render 的机会。


七、为什么 memo 之后子组件还是 render?

看一个很常见的例子。

子组件:

tsx 复制代码
const Child = memo(function Child({
  onDelete
}: {
  onDelete: () => void
}) {
  console.log('Child render')

  return (
    <button onClick={onDelete}>
      删除
    </button>
  )
})

父组件:

tsx 复制代码
function App() {
  const [count, setCount] = useState(0)

  function handleDelete() {
    console.log('delete')
  }

  return (
    <>
      <button onClick={() => setCount(count + 1)}>
        {count}
      </button>

      <Child onDelete={handleDelete} />
    </>
  )
}

看起来:

text 复制代码
handleDelete
逻辑完全没变

但每次 App 重新执行:

tsx 复制代码
function handleDelete() {
  console.log('delete')
}

都会创建一个新的函数对象。

也就是:

text 复制代码
上一次 handleDelete
!== 
这一次 handleDelete

虽然代码一样,但引用不一样。

因此:

text 复制代码
Child old props
≠
Child new props

于是 React.memo 失效,Child 仍然 render。


八、为什么函数每次 render 都可能是新对象?

JavaScript 中:

js 复制代码
const a = () => {}
const b = () => {}

console.log(a === b)

结果:

text 复制代码
false

因为:

text 复制代码
a 和 b
是两个不同的函数对象

React 组件重新执行时:

tsx 复制代码
function App() {
  function handleDelete() {
    ...
  }
}

每执行一次,就会重新创建函数。

所以:

text 复制代码
Render 1
→ function A

Render 2
→ function B

即使代码完全一样:

text 复制代码
A !== B

这就是 useCallback 出现的背景。


九、useCallback 到底解决什么问题?

可以改成:

tsx 复制代码
const handleDelete = useCallback(() => {
  console.log('delete')
}, [])

它表达的是:

如果依赖没有变化,尽量复用上一次那个函数引用。

于是:

text 复制代码
App render 1
↓
handleDelete = Function A

App render 2
↓
依赖没变
↓
handleDelete 仍然 = Function A

于是:

text 复制代码
Child old onDelete
===
Child new onDelete

React.memo 才有机会跳过 Child render。

所以:

text 复制代码
React.memo
→ 稳定组件

useCallback
→ 稳定函数引用

它们经常配合使用。


十、useCallback 本质不是"让函数执行更快"

这是非常常见的误区。

useCallback 不是:

优化这个函数内部的执行速度。

例如:

tsx 复制代码
const handleClick = useCallback(() => {
  console.log('hello')
}, [])

并不会让:

js 复制代码
console.log()

运行得更快。

它优化的是:

text 复制代码
函数引用稳定性

所以真正关注的是:

text 复制代码
这个函数是不是作为 props 传给 memo 组件?
这个函数是不是某些 Hook 的依赖?
函数引用变化是否真的造成额外成本?

如果都没有:

tsx 复制代码
useCallback(...)

可能毫无意义。


十一、useMemo 又是什么?

useMemouseCallback 很像,但缓存的东西不同。

例如:

tsx 复制代码
const completedTodos = useMemo(() => {
  return todos.filter(todo => todo.completed)
}, [todos])

含义:

text 复制代码
todos 没变
↓
复用上一次 completedTodos

todos 变了
↓
重新执行 filter
↓
保存新结果

所以:

text 复制代码
useMemo
→ 缓存计算结果

而:

text 复制代码
useCallback
→ 缓存函数引用

十二、三个 memo 到底分别缓存什么?

可以这样记:

text 复制代码
React.memo
→ Component

useMemo
→ Value

useCallback
→ Function

也可以进一步理解:

text 复制代码
React.memo
控制:
这个子组件需不需要重新执行?

useMemo
控制:
这个计算结果需不需要重新算?

useCallback
控制:
这个函数需不需要重新创建引用?

这是非常适合面试的一组对比。


十三、为什么 useMemo 不应该乱用?

例如:

tsx 复制代码
const total = todos.length

有人为了"性能优化"写:

tsx 复制代码
const total = useMemo(
  () => todos.length,
  [todos]
)

这种写法通常没有意义。

因为:

tsx 复制代码
todos.length

本身成本极低。

useMemo 自己也需要:

text 复制代码
保存上一次依赖
↓
比较依赖
↓
保存缓存结果
↓
维护 Hook 状态

优化本身也有成本。

所以:

优化不是免费的。

如果原计算:

text 复制代码
成本 1

你加了缓存机制:

text 复制代码
管理缓存成本 3

反而可能更复杂。


十四、什么时候 useMemo 更合理?

例如:

tsx 复制代码
const result = useMemo(() => {
  return hugeList
    .filter(...)
    .map(...)
    .sort(...)
}, [hugeList])

如果:

text 复制代码
数据量很大
计算明显昂贵
组件又经常因为其他 state re-render

那么 useMemo 才可能有价值。

也就是:

text 复制代码
组件频繁 re-render
+
计算成本高
+
计算依赖并没有变化

这时候缓存才真正节省计算。


十五、useMemo 还有一个很重要的场景:稳定对象引用

例如:

tsx 复制代码
const config = {
  pageSize: 20,
  keyword
}

每次 render:

text 复制代码
config

都是一个新的对象。

如果把它传给 memo 子组件:

tsx 复制代码
<Child config={config} />

即使:

text 复制代码
pageSize
keyword

没变:

text 复制代码
old config !== new config

memo 仍然可能失效。

可以:

tsx 复制代码
const config = useMemo(() => ({
  pageSize: 20,
  keyword
}), [keyword])

这样依赖不变时:

text 复制代码
config 引用稳定

所以 useMemo 不只是缓存昂贵计算。

它也可以:

稳定对象引用。


十六、把这些概念放回 TodoList

假设:

tsx 复制代码
<TodoList
  todos={filteredTodos}
  onToggle={toggleTodo}
  onDelete={deleteTodo}
/>

然后你:

tsx 复制代码
const TodoList = memo(...)

是否一定能避免重渲染?

不一定。

因为要检查三个 props:

text 复制代码
filteredTodos
onToggle
onDelete

filteredTodos

如果每次 render:

tsx 复制代码
const filteredTodos = todos.filter(...)

那么:

text 复制代码
filter()
每次都会产生一个新数组

即使内容一样:

text 复制代码
oldFilteredTodos
!== 
newFilteredTodos

于是 memo 可能失效。

可以在必要时:

tsx 复制代码
const filteredTodos = useMemo(() => {
  return todos.filter(...)
}, [todos, filter])

onToggle / onDelete

如果它们定义在组件内部:

tsx 复制代码
function toggleTodo() {}
function deleteTodo() {}

父组件每次重新执行,都可能创建新的函数引用。

如果确实需要配合 memo:

tsx 复制代码
const toggleTodo = useCallback(...)
const deleteTodo = useCallback(...)

这就是为什么 React 性能优化经常看到:

text 复制代码
React.memo
+
useMemo
+
useCallback

一起出现。


十七、但 TodoList 真的需要优化吗?

这是最重要的工程判断。

如果 Todo 只有:

text 复制代码
10 条
20 条
50 条

每次:

tsx 复制代码
todos.filter(...)

几乎没有成本。

TodoItem 组件也很简单。

这时候加入:

text 复制代码
memo
useMemo
useCallback

可能只会:

text 复制代码
代码更长
依赖更复杂
维护成本更高

却几乎没有性能收益。

因此更成熟的思路应该是:

不要因为"React 有这些 API"就主动全部使用。

正确流程:

text 复制代码
先写清晰正确的代码
↓
观察是否真的存在性能问题
↓
Profiler / 性能指标验证
↓
找到真正瓶颈
↓
针对性优化

十八、为什么 React 优化特别关注"引用"

因为 JavaScript 对象、数组、函数都是引用类型。

例如:

js 复制代码
{} === {}

结果:

text 复制代码
false
js 复制代码
[] === []

也是:

text 复制代码
false
js 复制代码
(() => {}) === (() => {})

仍然:

text 复制代码
false

所以 React 做 props 浅比较时:

text 复制代码
值内容一样
不代表引用一样

这就是:

text 复制代码
memo
useMemo
useCallback

经常围绕"引用稳定性"展开的原因。


十九、React 性能优化的核心不是"少 render"

还有一个很容易出现的误区:

render 越少越好。

其实不是。

React 的 render 本身经常很便宜。

真正需要关注的是:

text 复制代码
昂贵计算
大量组件重渲染
大量 DOM 更新
复杂布局
长列表
高频状态更新

如果一个组件重新执行只用了:

text 复制代码
0.05ms

你花大量复杂代码避免它 render,通常没有价值。

所以:

re-render 本身不是 bug。

真正的问题是:

不必要且昂贵的 re-render。


二十、面试:re-render 和 DOM 更新有什么区别?

可以这样回答:

React 的 re-render 指组件函数重新执行并计算新的 JSX 或 React Element 树,它不等同于真实 DOM 更新。React 会根据新旧渲染结果判断实际差异,只有真正发生变化的部分最终才会提交到 DOM。因此组件重新 render 并不意味着整个页面 DOM 被重新创建。

这个回答比:

React 有 Virtual DOM,所以比较快。

要准确很多。


二十一、面试:React.memo、useMemo、useCallback 有什么区别?

可以这样回答:

React.memo 用于组件级优化,当 props 没变化时可以跳过组件重新渲染;useMemo 用于缓存计算结果或稳定对象引用;useCallback 用于缓存函数引用。它们通常在 memoized child 或昂贵计算场景下使用,但都存在自身维护成本,不应该无脑使用,最好先通过实际性能问题和 Profiler 判断是否值得优化。


二十二、最终心智模型

把 React 渲染过程压缩成:

text 复制代码
State Change
↓
Component Re-render
↓
重新计算 JSX
↓
比较新旧结果
↓
Commit 必要 DOM 更新

而性能优化 API:

text 复制代码
React.memo
↓
这个子组件能不能不重新执行?

useMemo
↓
这个值能不能不重新计算?

useCallback
↓
这个函数引用能不能保持不变?

所以它们不是三个孤立 API。

它们都围绕同一个问题:

在 React 默认重新计算模型下,哪些工作可以安全地复用?


二十三、最后总结

学习 React 性能优化,正确顺序应该是:

text 复制代码
先理解 re-render
↓
理解父子组件更新关系
↓
理解引用类型
↓
理解 React.memo
↓
理解 useMemo
↓
理解 useCallback
↓
最后才考虑实际优化

而不是一上来就:

text 复制代码
所有组件 memo
所有函数 useCallback
所有计算 useMemo

真正成熟的 React 优化原则是:

先保证正确性和代码可读性,再用性能数据证明优化的必要性。

这也是理解 React 从"会用 Hook"走向"理解渲染模型"的关键一步。

相关推荐
IMPYLH1 小时前
HTML 的 <small> 元素
前端·网络·html
IT_陈寒1 小时前
Vue的响应式让我加班到凌晨,问题竟出在这个不起眼的地方
前端·人工智能·后端
Bs_MoneyMagnet5 小时前
基于springboot+vue的滑雪场票务与装备租赁系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring·毕业设计·计算机毕业设计
AlienZHOU10 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc13 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师14 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen15 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒15 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow15 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端