React: 性能优化:useMemo 与 useCallback
最后更新:2026-10-26
1. 你将学到
- useMemo 缓存计算结果
- useCallback 缓存函数引用
- React.memo 阻止不必要的重渲染
- 何时使用、何时滥用
- 性能分析的思路
2. 一个搜索过滤页面的故事
性能优化三件套的关系
flowchart TD
A[父组件渲染] --> B{Props 变了?}
B -->|是| C[React.memo 拦截?]
B -->|否| D[跳过子组件 ✅]
C -->|浅比较相等| D
C -->|浅比较不等| E{函数/对象是新的?}
E -->|是| F[useCallback/useMemo]
F --> G[稳定引用 → memo 生效 ✅]
E -->|否| D
style F fill:#e8f5e9,stroke:#2e7d32
style D fill:#c8e6c9,stroke:#2e7d32
style G fill:#c8e6c9,stroke:#2e7d32
(1) 痛点:每次输入都重新计算
Alice 做了一个商品搜索页面,有 10,000 件商品需要过滤:
JSX
function ShopPage() {
const [products] = useState(generate10000Products())
const [search, setSearch] = useState('')
const [sortBy, setSortBy] = useState('name')
// ❌ 问题:每次输入都重新过滤 10,000 件商品
const filtered = products
.filter(p => p.name.includes(search))
.sort((a, b) => a[sortBy] > b[sortBy] ? 1 : -1)
return (
<div>
<input value={search} onChange={e => setSearch(e.target.value)} />
<select value={sortBy} onChange={e => setSortBy(e.target.value)}>
<option value="name">按名称</option>
<option value="price">按价格</option>
</select>
<ProductList products={filtered} />
</div>
)
}
问题:每次键盘输入(setSearch)都会触发 filter 和 sort 重新执行——10,000 条数据每次都要过滤,界面卡顿。
(2) useMemo 的解法
JSX
function ShopPage() {
const [products] = useState(generate10000Products())
const [search, setSearch] = useState('')
const [sortBy, setSortBy] = useState('name')
// ✅ useMemo:只在 search 或 sortBy 变化时才重新计算
const filtered = useMemo(() => {
console.log('重新过滤...(只在搜索词或排序变化时)')
return products
.filter(p => p.name.includes(search))
.sort((a, b) => a[sortBy] > b[sortBy] ? 1 : -1)
}, [search, sortBy, products])
return (
<div>
<input value={search} onChange={e => setSearch(e.target.value)} />
<select value={sortBy} onChange={e => setSortBy(e.target.value)}>
<option value="name">按名称</option>
<option value="price">按价格</option>
</select>
<ProductList products={filtered} />
</div>
)
}
收益:每次输入只在 search 或 sortBy 变化时才重新过滤,其他状态变化(比如父组件的重渲染)不会触发过滤,性能提升 10-100 倍。
3. useMemo:记住计算结果
| Hook | 记住什么 | 返回值 | 依赖变化时 |
|---|---|---|---|
useMemo |
计算结果 | 缓存的值 | 重新计算 |
useCallback |
函数引用 | 缓存的函数 | 创建新函数 |
React.memo |
整个组件渲染结果 | 跳过重渲染 | props 变化时重新渲染 |
(1) 基本语法
JSX
const memoizedValue = useMemo(() => {
return 昂贵的计算()
}, [依赖1, 依赖2])
| 部分 | 说明 |
|---|---|
() => 计算 |
需要缓存的函数,返回计算结果 |
[依赖] |
依赖变化时重新计算 |
memoizedValue |
缓存的结果(依赖没变时直接返回上次结果) |
(2) 适用场景
JSX
// 场景 1:数据转换(过滤、排序、分组)
const activeUsers = useMemo(
() => users.filter(u => u.isActive).sort((a, b) => a.name.localeCompare(b.name)),
[users]
)
// 场景 2:格式化(数字、日期、字符串拼接)
const formattedPrice = useMemo(
() => new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(price),
[price]
)
// 场景 3:派生状态(从已有数据计算新数据)
const statistics = useMemo(() => ({
total: orders.length,
paid: orders.filter(o => o.status === 'paid').length,
amount: orders.reduce((sum, o) => sum + o.amount, 0),
avgAmount: orders.length ? orders.reduce((sum, o) => sum + o.amount, 0) / orders.length : 0
}), [orders])
输出:
TEXT
...
▶ 示例:性能对比
JSX
// ============================================
// 示例:有 useMemo vs 无 useMemo 的性能对比
// ============================================
function ExpensiveCalc({ num, unrelated }) {
// ❌ 没有 useMemo:每次渲染都执行
const slowResult1 = slowFibonacci(num)
// ✅ 有 useMemo:num 没变就不重新计算
const slowResult2 = useMemo(() => slowFibonacci(num), [num])
return (
<div>
<p>num={num} 的斐波那契数列</p>
<p>无 useMemo:{slowResult1}(每次渲染计算)</p>
<p>有 useMemo:{slowResult2}(只在 num 变化时计算)</p>
<p>不相关状态:{unrelated}(点按钮改变这个值)</p>
</div>
)
}
function slowFibonacci(n) {
if (n <= 1) return n
let a = 0, b = 1, c
for (let i = 2; i <= n; i++) { c = a + b; a = b; b = c }
console.log('fibonacci 计算!n=' + n)
return b
}
// 点击改变 unrelated → 无 useMemo 的重新计算,有 useMemo 的跳过
输出:
TEXT
Fibonacci(n):不使用 useMemo 每次渲染都重新计算,使用 useMemo 时仅在无关状态变化时跳过。控制台:"fibonacci calculated! n=X"
4. useCallback:记住函数引用
(1) 基本语法
JSX
const memoizedFn = useCallback(() => {
doSomething(a, b)
}, [a, b])
useCallback(fn, deps) 等价于 useMemo(() => fn, deps) ——记住的是函数本身,而不是函数的结果。
(2) 为什么需要 useCallback
JSX
function Parent() {
const [count, setCount] = useState(0)
// ❌ 每次 Parent 渲染,handleClick 都是新的函数
// 导致 Child 的 React.memo 失效
const handleClick = () => {
console.log('点击了')
}
// ✅ useCallback:handleClick 引用不变
// Child 的 React.memo 能正常工作(跳过重渲染)
const handleClick = useCallback(() => {
console.log('点击了')
}, []) // 空依赖:这个函数永远不会变
}
(3) React.memo:阻止子组件重渲染
▶ 试一试5. 什么时候用,什么时候别用
(1) 应该用的情况
| 场景 | Hook | 原因 |
|---|---|---|
| 大数据过滤/排序 | useMemo |
避免每次渲染都遍历大量数据 |
| 复杂计算(数学/格式化) | useMemo |
计算结果不变就不需要重算 |
| Props 传给 React.memo 子组件 | useCallback |
保持函数引用稳定,让 React.memo 生效 |
| useEffect 依赖中的函数 | useCallback |
避免 useEffect 因函数引用变化而重复执行 |
(2) 不需要用的情况
JSX
// ❌ 不需要:简单计算
const doubled = useMemo(() => count * 2, [count])
// ✅ 直接写:count * 2 本身就已经很快了
const doubled = count * 2
// ❌ 不需要:事件处理但不传给子组件
const handleClick = useCallback(() => {
console.log('点击')
}, [])
// ✅ 直接写即可
const handleClick = () => console.log('点击')
// ❌ 不需要:子组件没有用 React.memo
// useCallback 包装了但没人消费这个"稳定引用"
(3) 滥用的代价
JSX
// useMemo 和 useCallback 本身有开销(比较依赖数组)
// 滥用 = 引入额外的比较逻辑,比不优化更慢
// 经验法则:
// 1. 先写不加优化的版本
// 2. 感觉到卡顿(帧率 < 60fps)再用
// 3. 用 React DevTools Profiler 测量
6. 完整示例:聊天应用消息列表
▶ 展开运行示例(112 行)
输出:
TEXT
... ('fibonacci Calculation!n=' + n)
预期输出:100 条消息的聊天列表。点击"增加未读数"时,由于 useCallback + React.memo,消息列表不会重新渲染(控制台无输出),只有统计数字变化。
▶ 示例 2:useCallback 防止 useEffect 重复执行
▶ 试一试输出:
TEXT
搜索框 + 分页。输入 "React" → 3 条结果("Results React - Page 1" 等)。useCallback 防止 useEffect 无限循环。Prev/Next 按钮。
输出:
TEXT
副标题:"🔍 Search(useCallback Demo)"。显示:"🔍 Search(useCallback Demo)"。状态:query(setter:setQuery)、results(setter:setResults)、page(setter:setPage)。按钮:setPage(p => p + 1)}>Next Page。输入:Enter a search term.... 列表:{r.title}。useEffect 管理副作用。useCallback 缓存处理函数引用。异步数据获取/加载状态
▶ 示例 3:useMemo 实现虚拟列表计算
JSX
function VirtualList({ items, itemHeight = 40, visibleHeight = 400 }) {
const [scrollTop, setScrollTop] = useState(0)
const visibleCount = Math.ceil(visibleHeight / itemHeight)
const totalHeight = useMemo(() => items.length * itemHeight, [items.length, itemHeight])
const { startIndex, endIndex, offsetY } = useMemo(() => {
const start = Math.floor(scrollTop / itemHeight)
const end = Math.min(start + visibleCount + 1, items.length)
return { startIndex: start, endIndex: end, offsetY: start * itemHeight }
}, [scrollTop, itemHeight, visibleCount, items.length])
const visibleItems = useMemo(() => items.slice(startIndex, endIndex), [items, startIndex, endIndex])
return (
<div onScroll={e => setScrollTop(e.currentTarget.scrollTop)}
style={{ height: visibleHeight, overflowY: 'auto', border: '1px solid #ddd', borderRadius: 4 }}>
<div style={{ height: totalHeight, position: 'relative' }}>
<div style={{ position: 'absolute', top: offsetY, width: '100%' }}>
{visibleItems.map((item, i) => (
<div key={startIndex + i} style={{ height: itemHeight, padding: '0 12px', display: 'flex', alignItems: 'center', borderBottom: '1px solid #f0f0f0' }}>
Row {startIndex + i}: {item}
</div>
))}
</div>
</div>
</div>
)
}
输出:
TEXT
虚拟滚动:只渲染可见的行(1000+ 行中约 10 行)。滚动 → 行随之替换。总高度 = items × 40px。useMemo 计算可见范围。
输出:
TEXT
显示:"items.slice(startIndex, endIndex), [items, startIndex, endIn"。状态:scrollTop(setter:setScrollTop)。useMemo 优化计算
▶ 示例 4:React.memo + useCallback 优化列表
▶ 试一试输出:
TEXT
商品列表(Keyboard $79、Mouse $49、Monitor $399、Headphones $129)带筛选功能。"Remove" 删除该项。React.memo + useCallback = 只有发生变化的项才重新渲染。
输出:
TEXT
渲染:...
▶ 示例 5:何时不用 useMemo——反模式对比
JSX
function BadExample({ count }) {
// ❌ 简单计算不需要 useMemo——创建 memo 开销 > 计算本身
const doubled = useMemo(() => count * 2, [count])
// ✅ 直接计算
const doubledOk = count * 2
// ❌ 每次渲染创建新对象作为 useMemo 依赖
const config = useMemo(() => ({ count }), [count])
const badResult = useMemo(() => config.count * 3, [config]) // config 每次是新引用!
// ✅ 直接用基本类型作为依赖
const goodResult = useMemo(() => count * 3, [count])
return <p>Doubled: {doubledOk}, Tripled: {goodResult}</p>
}
function GoodExample({ items, query }) {
// ✅ 过滤/排序大量数据用 useMemo
const filtered = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(query.toLowerCase())
).sort((a, b) => a.name.localeCompare(b.name))
}, [items, query])
// ✅ 传递给子组件的复杂对象用 useMemo
const chartData = useMemo(() => ({
labels: filtered.map(i => i.name),
values: filtered.map(i => i.price),
}), [filtered])
return (
<div>
<p>Found: {filtered.length} items</p>
<Chart data={chartData} />
</div>
)
}
输出:
TEXT
❌ 对 count*2 使用 useMemo(开销 > 计算本身)。❌ 用新对象作为依赖会破坏 memo。✅ 对大规模过滤/排序以及派生的图表数据对象使用 useMemo。
❓ 常见问题
Q useMemo 和 useCallback 一定能提升性能吗?
A 不一定。它们本身也有开销——每次渲染都要比较依赖数组。滥用反而变慢。React 官方建议:先写不加优化的版本,感觉到卡顿再优化。Rule of thumb:只有传给 React.memo 子组件或开销 > 1ms 的计算才需要用。
Q useMemo 和直接变量有什么区别?
A useMemo 有"记忆"——依赖没变就直接用缓存值,不重新执行计算。直接变量每次渲染都重新计算。但 useMemo 需要在渲染时比较依赖数组,所以"简单的计算用 useMemo 反而更慢"。
Q 什么时候用 useCallback 保函数引用?
A 只有当同时满足两个条件时才需要:① 函数作为 Props 传给 React.memo 子组件;② 函数在 useEffect 的依赖数组中。不满足这两个条件时,直接写函数即可,useCallback 是多余的。
Q useMemo 和 useCallback 有什么区别?
A
useMemo(() => value, deps) 缓存计算结果(值),useCallback(() => fn, deps) 缓存函数引用。实际上 useCallback(fn, deps) 等价于 useMemo(() => fn, deps)。当你需要缓存一个函数传给子组件时用 useCallback;当你需要缓存一个计算结果时用 useMemo。📖 小节
useMemo缓存计算结果,依赖不变就不重新计算useCallback缓存函数引用,配合 React.memo 阻止子组件重渲染React.memo对 Props 做浅比较,Props 没变就跳过子组件渲染- 不要过早优化——先写能跑的代码,卡顿了再优化
- 用 React DevTools Profiler 识别性能瓶颈
📝 作业
- 基础题(难度⭐):创建一个
PrimeCalculator组件,输入数字 n,计算第 n 个质数。用 useMemo 缓存结果,避免每次输入都重新计算。 - 进阶题(难度⭐⭐):创建一个
ProductTable组件,有 500 条商品数据,支持按分类筛选和按价格排序。用 useMemo 优化过滤/排序逻辑,用 useCallback + React.memo 优化列表渲染。 - 挑战题(难度⭐⭐⭐):创建一个
DataDashboard组件,从 3 个不同维度(时间、地区、类别)聚合 10,000 条订单数据,用 useMemo 实现分层缓存(维度变化时只重新计算相关层)。