React useCallback 与 useMemo 性能优化完全指南
React useCallback 与 useMemo 性能优化完全指南
一、Introduction
React 应用性能问题的根源,往往在于不必要的重新渲染(re-render):父组件状态变化导致子组件也跟着重新渲染,即使子组件的 props 根本没变。这时候 useCallback 和 useMemo 就派上用场了——它们通过缓存函数和计算结果,让组件在 props 未变化时跳过重新渲染。
但注意:不是所有组件都需要 memoization。过度使用这两个 Hook 会增加内存负担和维护复杂度。什么时候该用、什么时候不该用,是本文要解答的核心问题。
---
二、useCallback:缓存回调函数
#### 2.1 为什么要缓存回调函数?
当一个函数作为 props 传给子组件时,如果每次渲染都创建新函数,即使函数逻辑完全相同,子组件也会认为 props 变了(因为引用不同),从而触发不必要的重新渲染。
import React, { useState, useCallback } from 'react';
// 子组件:接受 onIncrement 作为 prop
function CounterButton({ onIncrement, label }) {
console.log(`Button [${label}] rendered`);
return <button onClick={onIncrement}>{label}</button>;
}
// 用 React.memo 包装,让子组件在 props 不变时跳过渲染
const MemoizedButton = React.memo(CounterButton);
function Parent() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
// 不用 useCallback:每次渲染都创建新函数,导致 MemoizedButton 重新渲染
const handleIncrementNaive = () => {
setCount(c => c + step);
};
// 用 useCallback:只有在 step 变化时才创建新函数
// count 变化时,handleIncrement 保持同一个引用
const handleIncrementOptimized = useCallback(() => {
setCount(c => c + step);
}, [step]);
return (
<div>
<p>计数:{count},步长:{step}</p>
{/* 每次 count 变化,handleIncrementNaive 都是新引用
→ 即使 MemoizedButton 也会重新渲染(除非你用 React.memo) */}
<MemoizedButton
onIncrement={handleIncrementNaive}
label="普通方式"
/>
{/* handleIncrementOptimized 只有 step 变化时才变
→ MemoizedButton 可以跳过不必要的渲染 */}
<MemoizedButton
onIncrement={handleIncrementOptimized}
label="优化方式"
/>
<button onClick={() => setStep(s => s + 1)}>增加步长</button>
</div>
);
}
export default Parent;
---
三、useMemo:缓存计算结果
#### 3.1 基础用法
useMemo 用于缓存昂贵计算的结果,避免每次渲染都重新计算:
import React, { useState, useMemo } from 'react';
function ExpensiveCalculation() {
const [number, setNumber] = useState(5);
const [multiplier, setMultiplier] = useState(2);
// 模拟一个昂贵的计算(实际项目中可能是复杂的排序、筛选、大数据处理)
const computeFactorial = (n) => {
console.log('执行昂贵计算...');
if (n <= 1) return 1;
let result = 1;
for (let i = 2; i <= n; i++) {
result *= i;
}
return result;
};
// 不用 useMemo:number 或 multiplier 任何一个变化,都会重新计算
const factorialNaive = computeFactorial(number);
// 用 useMemo:只有 number 变化时才重新计算
// multiplier 变化不会触发 factorialMemo 重新计算
const factorialMemo = useMemo(() => {
console.log('useMemo 计算...');
return computeFactorial(number);
}, [number]);
return (
<div>
<p>数字:{number},乘数:{multiplier}</p>
<p>普通计算 factorial({number}) = {factorialNaive}</p>
<p>useMemo 缓存 factorial({number}) = {factorialMemo}</p>
<button onClick={() => setNumber(n => Math.max(1, n - 1))}>数字-1</button>
<button onClick={() => setNumber(n => n + 1)}>数字+1</button>
<button onClick={() => setMultiplier(m => m + 1)}>乘数+1(只影响显示,不重新计算 factorial)</button>
</div>
);
}
export default ExpensiveCalculation;
---
四、useCallback 与 useMemo 的正确使用场景
#### 4.1 性能分析先行,不要猜测
React 官方推荐先用 React DevTools Profiler 或 Chrome Performance 标签进行性能分析,确认确实存在性能问题后再使用这些优化 Hook。过度优化反而浪费资源。
#### 4.2 什么时候该用
// 场景一:回调函数作为子组件的 props,且子组件用 React.memo 包装
const handleSubmit = useCallback(() => {
submitForm(data);
}, [data]);
// 场景二:useEffect 的依赖是函数,用 useCallback 包裹确保依赖稳定
const handleDataChange = useCallback((newData) => {
setData(processData(newData));
}, []);
useEffect(() => {
window.addEventListener('resize', handleDataChange);
return () => window.removeEventListener('resize', handleDataChange);
}, [handleDataChange]);
// 场景三:渲染大量数据时的派生计算(排序、筛选、聚合)
const sortedList = useMemo(() => {
return [...items].sort((a, b) => a.price - b.price);
}, [items]);
const filteredList = useMemo(() => {
return items.filter(item => item.category === selectedCategory);
}, [items, selectedCategory]);
#### 4.3 什么时候不该用
// 错误用法一:简单计算不需要 useMemo,引入的开销可能比计算本身还大
const doubled = useMemo(() => count * 2, [count]); // 普通乘法不需要缓存
// 错误用法二:对象字面量每次渲染都是新引用,无法有效缓存
const style = useMemo(() => ({ color: 'red' }), []); // 实际用处不大
// 错误用法三:在 useMemo/useCallback 内部的函数如果依赖频繁变化的对象,
// 缓存效果极差,不如直接写
---
五、常见问题(FAQ)
#### Q1:useCallback 和 useMemo 的区别是什么?
useCallback 缓存函数本身(返回稳定的函数引用),本质是 useMemo(() => fn, deps) 的语法糖;useMemo 缓存任意计算结果(数字、对象、JSX 等)。如果你要缓存的是回调函数,两者等价,但 useCallback 语义更清晰。
// 两者等价:
const handleClick = useCallback(() => { doSomething(a); }, [a]);
const handleClick = useMemo(() => () => { doSomething(a); }, [a]);
#### Q2:useMemo 的依赖数组可以为空吗?
可以。空依赖数组 [] 表示这个值永远不重新计算(永远返回第一次计算的结果)。但要注意:如果这个值引用了组件内部定义的对象或函数(它们每次渲染都是新引用),空依赖会导致"过期闭包"问题——useMemo 持有的是过时的引用。
// 正确:缓存一个不依赖任何渲染时变量的常量
const allCountries = useMemo(() => ['China', 'USA', 'Japan'], []);
// 危险:缓存了一个依赖外部变量的值,但声明依赖为空
const total = useMemo(() => price * discount, []); // price 变化时 total 不更新!
#### Q3:React.memo 和 useMemo 有什么关系?
React.memo 是一个高阶组件,用于包装子组件,让它在 props 没变时跳过渲染。useMemo 是一个 Hook,用于缓存计算结果或函数引用。两者常配合使用:父组件用 useCallback 稳定传给子组件的回调,子组件用 React.memo 避免无意义的渲染。
// 父组件:用 useCallback 稳定回调引用
const handleClick = useCallback(() => doSomething(id), [id]);
// 子组件:用 React.memo 跳过 props 未变化的渲染
const Button = React.memo(({ onClick, label }) => {
console.log('Button rendered');
return <button onClick={onClick}>{label}</button>;
});
---
六、延伸阅读
- [React Hooks 完全指南:useState 与 useEffect 核心用法]()
- [React useRef 完全指南:操作 DOM 与持久化变量]()
- [React 组件设计模式:高阶组件与 Render Props]()
- [React 性能优化实战:Profiler、渲染分析与优化策略]()
---