useMemo 和 useCallback 是 React 提供的两个用于性能优化的 Hook。它们都利用缓存(memoization)机制来避免不必要的重新计算或重新创建,但在实际开发中,它们经常被误用,甚至反而降低了代码质量和可维护性。理解它们的正确使用场景和常见误区,是写出高效 React 代码的关键。
useMemo:缓存昂贵的计算结果
useMemo 接受一个“创建函数”和一个依赖数组,只有当依赖项发生变化时,才会重新执行创建函数,否则直接返回上次缓存的值。
基本语法:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
适用场景:
- 真正的计算密集型任务
对大量数据进行排序、过滤、复杂转换,且该计算并不需要在每次渲染时都执行。
function ProductList({ products, filterText }) {
// 只有当 products 或 filterText 改变时才重新筛选排序
const filtered = useMemo(() => {
return products
.filter(p => p.name.includes(filterText))
.sort((a, b) => b.price - a.price);
}, [products, filterText]);
return <List data={filtered} />;
}
- 引用稳定性(避免子组件不必要的重渲染)
当把一个对象或数组作为 Props 传递给使用 React.memo 包裹的子组件时,如果该引用在每次渲染时都重新创建,会导致子组件即使内容相同也会被重新渲染。
const styles = useMemo(() => ({ color: 'red', fontWeight: 'bold' }), []);
<ExpensiveChild style={styles} />;
useMemo 保证了在依赖不变的情况下返回同一个引用,从而配合 React.memo 的浅比较跳过子组件渲染。
常见误区:
- 过早优化
大多数情况下,JavaScript 的计算速度远快于 DOM 操作,简单的加法、字符串拼接根本无需缓存。加上 useMemo 本身也需要进行依赖比较和内存占用,可能得不偿失。
❌ 错误示例:
const total = useMemo(() => price + tax, [price, tax]);
- 依赖数组遗漏或不完整
如果依赖数组遗漏了内部使用的变量,会导致缓存失效或始终使用旧值,引发难以排查的 bug。ESLint 的 react-hooks/exhaustive-deps 规则能帮助检查,务必遵守。
- 把 useMemo 当成语义保证
React 文档明确说明:useMemo 只作为性能优化手段,React 可能在将来为了释放内存而丢弃缓存值(尽管当前实现不会)。因此不能依赖它的缓存行为来保证逻辑正确性。
useCallback:缓存函数引用
useCallback 本质上是对 useMemo 的语法糖,专用于缓存函数引用。如果传给子组件的函数每次渲染都重新创建,同样会破坏 React.memo 的优化效果。
基本语法:
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
适用场景:
- 传递给
React.memo子组件的回调函数
当父组件状态变化时,如果不缓存传递给子组件的函数,子组件哪怕其他数据没变,也会因为函数引用不同而重新渲染。
function Parent() {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(c => c + 1);
}, []); // 空依赖,该函数永远不变
return <Child onIncrement={increment} />;
}
const Child = React.memo(({ onIncrement }) => {
console.log('child render');
return <button onClick={onIncrement}>+1</button>;
});
- 作为其他 Hook 的依赖(如
useEffect)
如果你在 useEffect 中调用了某个函数,并把它列入了依赖数组,那么该函数需要使用 useCallback 缓存,否则每次渲染都会触发副作用重新执行。
const fetchData = useCallback(async () => {
const res = await api.getData(id);
// ...
}, [id]);
useEffect(() => {
fetchData();
}, [fetchData]);
常见误区:
- 无差别包裹所有函数
许多新手看到子组件就用 useCallback 包裹所有回调,但只有当子组件被 React.memo 包裹时才有意义(或使用了浅比较的其他优化)。普通子组件每次都会重新渲染,缓存反而增加了无用的成本。
- 忽略闭包陷阱
当 useCallback 依赖数组中的值发生变化时,新创建的函数会捕获最新的 state;但如果依赖数组不变,函数中引用的 state 就会是旧的。必须确保依赖项完整,否则会出现“读不到最新值”的问题。
const [text, setText] = useState('');
const log = useCallback(() => {
console.log(text); // 如果 text 没有在依赖中,这里永远打印初始值
}, []); // ❌ 遗漏依赖
- 滥用导致依赖链复杂
为了做到依赖完整,开发者常常在 useCallback 内部使用很多 state 和 props,导致依赖数组变得臃肿,进而产生连锁的 useCallback 和 useMemo,使代码难以维护。有时直接使用函数式更新或重构组件结构更为简洁。
useMemo 与 useCallback 的本质与取舍
- 本质相同:
useCallback(fn, deps)等价于useMemo(() => fn, deps)。 - 何时用哪个:如果返回值是普通数据(对象、数组、数字等),用
useMemo;如果返回值本身就是一个函数,且需要保持引用稳定,用useCallback。 - 先写清洁代码,再优化
在性能问题实际出现之前,不必急于使用它们。大多数应用在中等复杂度的交互下,React 的默认更新已经很快。当通过 React DevTools Profiler 定位到具体的不必要的重渲染时,再有针对性地引入缓存。
最佳实践速查
| 场景 | 推荐做法 |
|------|----------|
| 计算量大且依赖明确的数据转换 | 使用 useMemo |
| 将对象/数组作为 Props 传给 React.memo 子组件 | 使用 useMemo 保持引用稳定 |
| 将回调函数传给 React.memo 子组件 | 使用 useCallback |
| 函数被用在 useEffect / 其他 Hook 的依赖数组里 | 使用 useCallback |
| 纯视图组件无重渲染问题 | 无需使用两者 |
| 简单运算或零成本操作 | 直接计算,不要用缓存 |
总结
useMemo 和 useCallback 是精准的性能手术刀,而不是日常的保健品。在没有必要的地方使用它们,不仅增加了代码复杂度,还可能因为额外的依赖管理引发 bug。记住一条简单原则:只有当确实存在可测量的性能问题时,才打开这些缓存工具;同时务必通过 ESLint 规则保证依赖数组的正确性。