很多人写 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 又是什么?
useMemo 和 useCallback 很像,但缓存的东西不同。
例如:
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"走向"理解渲染模型"的关键一步。