React 中,useMemo 和 useCallback 的区别?什么时候使用?
精炼回答
useMemo 和 useCallback 都是 React 中用于性能优化的 Hook,但它们缓存的对象不同:
useMemo缓存的是计算结果。useCallback缓存的是函数引用。
具体来说,useMemo 会执行传入的计算函数并缓存其返回值。在通常情况下,只要依赖项没有变化,React 就会复用之前缓存的结果;当依赖项发生变化时,则重新执行计算函数。
例如:
const activeItems = useMemo(
() => items.filter(item => item.active),
[items]
);
JavaScript这样可以避免在依赖项未变化时重复执行相同的过滤计算。
useCallback 则用于缓存函数引用。在通常情况下,只要依赖项没有变化,React 就会返回之前缓存的函数引用。
例如:
const handleSelect = useCallback((id) => {
setSelectedId(id);
}, []);
JavaScript它常用于以下场景:
- 将函数作为 prop 传给使用
React.memo包装的子组件。 - 某个函数需要作为其他 Hook 的依赖项。
- 某些依赖函数引用稳定性的第三方组件或自定义 Hook。
简单来说:
| Hook | 缓存内容 | 常见用途 |
|---|---|---|
useMemo | 计算结果 | 避免重复执行昂贵计算、稳定对象或数组引用 |
useCallback | 函数引用 | 稳定回调函数引用、配合 React.memo、作为其他 Hook 的依赖 |
需要注意的是,useMemo 和 useCallback 本身也存在依赖比较、缓存维护等开销,因此不应该机械地给所有计算和函数都加上记忆化。
优先在计算成本较高、需要稳定引用、与 React.memo 配合,或者函数需要作为其他 Hook 依赖项等明确场景中使用。对于性能敏感场景,最好使用 React DevTools Profiler 等工具验证实际收益。
扩展分析
详细解释
掌握这两个 Hook 的关键,在于理解它们解决的是两个不同的问题:
useMemo主要解决重复计算问题。useCallback主要解决函数引用不稳定问题。
在组件后续渲染时,React 会使用 Object.is 将依赖数组中的每一项与上一次渲染时对应的依赖项进行比较。
只要其中任意依赖发生变化:
useMemo会重新执行计算函数,并缓存新的计算结果。useCallback会返回当前渲染中传入的新函数引用。
因此,更准确的描述不是“React 对依赖数组进行浅比较”,而是:
React 会逐项使用
Object.is比较依赖。
对于基本类型,例如:
1
'hello'
true
JavaScriptReact 比较的是值。
对于对象、数组和函数,如果每次渲染都重新创建,即使内容完全相同,其引用也通常不同,因此会被判断为依赖发生了变化。
例如:
const options = {
sort: 'price'
};
JavaScript如果这段代码直接写在组件函数内部,那么每次渲染都会创建一个新的对象。
因此:
useMemo(() => {
// ...
}, [options]);
JavaScript中的 options 很可能每次渲染都会发生变化,从而导致 useMemo 失去预期的缓存效果。
可以把依赖变化过程理解为:
组件重新渲染
↓
React 使用 Object.is
逐项比较依赖
↙ ↘
无变化 有变化
↓ ↓
复用缓存 重新执行 / 返回新引用
↓
更新缓存
↘ ↙
组件完成渲染
需要注意的是,useMemo 和 useCallback 都属于性能优化手段,而不是业务语义保证。
因此,不应该依赖它们来保证程序逻辑正确性。即使缓存被丢弃,组件逻辑也应该仍然能够正确工作。
useMemo:缓存计算结果
useMemo 的性能收益主要来自避免重复执行昂贵计算。
例如:
const result = useMemo(() => {
return expensiveCalculation(data);
}, [data]);
JavaScript当 data 没有变化时,React 通常会复用之前的 result,而不是再次执行 expensiveCalculation。
但使用 useMemo 是否值得,取决于:
重新计算的成本
VS
依赖比较 + 缓存维护的成本
如果只是:
const total = price * count;
JavaScript或者:
const names = users.map(user => user.name);
JavaScript而数据规模又非常小,那么使用 useMemo 往往没有明显收益,反而增加了代码复杂度。
但如果是:
- 大量数据过滤。
- 多条件排序。
- 深度递归计算。
- 复杂数据转换。
- 大型图表数据预处理。
那么 useMemo 可能具有实际价值。
useCallback:缓存函数引用
JavaScript 中,函数也是对象。
例如:
function Parent() {
const handleClick = () => {
console.log('clicked');
};
return <Child onClick={handleClick} />;
}
JavaScript每次 Parent 重新渲染时:
const handleClick = () => {
console.log('clicked');
};
JavaScript都会创建一个新的函数对象。
也就是说:
previousHandleClick === currentHandleClick
JavaScript结果通常为:
false
JavaScript如果 Child 使用了 React.memo:
const Child = React.memo(function Child({ onClick }) {
return <button onClick={onClick}>Click</button>;
});
JavaScript父组件每次创建新的 handleClick,都会导致 onClick 这个 prop 被判断为发生了变化。
这并不意味着 React.memo “失效”,而是:
由于函数 prop 的引用发生了变化,
React.memo无法仅依靠 props 相等来跳过这一次子组件渲染。
此时可以使用:
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
JavaScript只要依赖没有变化,React 通常会保持函数引用稳定。
实践应用
场景一:复杂商品筛选
在电商场景中,经常需要根据多个条件筛选大量商品:
const filteredProducts = useMemo(() => {
return products
.filter(product => {
return (
product.price >= priceRange[0] &&
product.price <= priceRange[1] &&
selectedBrands.includes(product.brand) &&
product.rating >= minRating
);
})
.sort((a, b) => a.price - b.price);
}, [products, priceRange, selectedBrands, minRating]);
JavaScript假设 products 包含几千条数据,每次过滤还包含品牌判断、价格区间判断、评分判断和排序,那么这类计算就可能值得使用 useMemo。
不过需要特别注意 priceRange:
const priceRange = [minPrice, maxPrice];
JavaScript如果父组件在每次渲染时都创建一个新的数组,那么即使:
minPrice
maxPrice
JavaScript完全没有变化,priceRange 的引用仍然可能不同,从而导致 useMemo 重新执行。
可以考虑直接拆成基本类型:
const filteredProducts = useMemo(() => {
return products
.filter(product => {
return (
product.price >= minPrice &&
product.price <= maxPrice &&
selectedBrands.includes(product.brand) &&
product.rating >= minRating
);
})
.sort((a, b) => a.price - b.price);
}, [products, minPrice, maxPrice, selectedBrands, minRating]);
JavaScript或者确保 priceRange 本身的引用稳定。
场景二:稳定对象引用
另一个常见场景是向子组件传递配置对象。
例如:
const chartConfig = useMemo(() => ({
type: 'line',
data: salesData,
options: {
responsive: true,
scales: {
y: {
beginAtZero: true
}
}
}
}), [salesData]);
JavaScript如果这个配置对象需要传给一个依赖引用稳定性的组件:
<Chart config={chartConfig} />
JavaScript使用 useMemo 可以避免父组件每次渲染都重新创建一个新的配置对象。
但这里仍然要注意:
只有当对象引用稳定确实能够避免额外工作时,
useMemo才有意义。
如果 Chart 本身每次都会重新渲染,而且配置对象创建成本极低,那么这个优化可能没有实际收益。
场景三:稳定事件处理函数
useCallback 最常见的用途之一,是稳定传递给子组件的事件处理函数。
例如:
const handleProductClick = useCallback((productId) => {
setSelectedProduct(productId);
analytics.track('product_click', { productId });
}, [analytics]);
JavaScript然后:
products.map(product => (
<ProductCard
key={product.id}
product={product}
onClick={handleProductClick}
/>
));
JavaScript如果 ProductCard 使用:
const ProductCard = React.memo(function ProductCard({
product,
onClick
}) {
// ...
});
JavaScript那么稳定的 onClick 引用可以减少因为函数引用变化导致的额外渲染。
这里需要特别注意依赖数组。
如果:
analytics
JavaScript来自组件作用域中的 prop、state、context 或其他 reactive value,那么应该将其加入依赖数组:
[analytics]
JavaScript而:
setSelectedProduct
JavaScript如果来自:
const [selectedProduct, setSelectedProduct] = useState(null);
JavaScriptReact 保证 state setter 的函数引用稳定,因此通常可以省略。
如果 analytics 是模块级导入的稳定单例:
import analytics from './analytics';
JavaScript则它不是组件内部的 reactive dependency,此时可以写:
const handleProductClick = useCallback((productId) => {
setSelectedProduct(productId);
analytics.track('product_click', { productId });
}, []);
JavaScript实际项目中应结合 ESLint 的 react-hooks/exhaustive-deps 规则检查依赖,而不是凭感觉手工删除依赖。
常见误区
误区一:所有函数都应该使用 useCallback
错误思路:
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);
JavaScript然后组件里所有函数都这样写。
实际上,如果:
- 这个函数没有传给记忆化子组件。
- 它没有作为其他 Hook 的依赖。
- 创建函数本身成本很低。
- 子组件本身渲染也非常快。
那么使用 useCallback 往往没有实际价值。
误区二:所有计算都应该使用 useMemo
例如:
const fullName = useMemo(() => {
return `${firstName} ${lastName}`;
}, [firstName, lastName]);
JavaScript这种计算极其简单,一般没有必要使用 useMemo。
直接:
const fullName = `${firstName} ${lastName}`;
JavaScript通常更加清晰。
误区三:对象内容一样,React 就认为依赖没变
例如:
const filters = {
keyword,
category
};
JavaScript即使两次渲染时:
filters.keyword
filters.category
JavaScript的值完全相同,只要创建了新的对象:
previousFilters !== currentFilters
JavaScript依赖就会被判断为发生变化。
因此不要只关注对象内容,还要关注引用是否稳定。
误区四:useCallback 一定能减少子组件渲染
并不是。
假设:
const handleClick = useCallback(() => {
// ...
}, []);
JavaScript虽然 handleClick 稳定了,但如果子组件还有其他 prop 每次都在变化:
<Child
onClick={handleClick}
options={{ theme: 'dark' }}
/>
JavaScript这里:
options={{ theme: 'dark' }}
JavaScript每次渲染都会创建新对象。
因此即使 onClick 没变化,React.memo 仍然可能因为 options 变化而重新渲染。
误区五:React.memo 可以保证组件不重新渲染
不能。
React.memo 是一种性能优化机制。
默认情况下,它会比较组件的新旧 props。如果 props 被判断为相同,React 通常可以跳过该组件由父组件更新触发的重新渲染。
但组件自身的:
- state 变化。
- context 变化。
- 其他 React 内部调度行为。
仍然可能导致组件重新渲染。
因此不要把 React.memo 当作组件“不重新渲染”的语义保证。
useMemo 与 useCallback 的关系
从概念上看:
useCallback(fn, dependencies)
JavaScript可以理解为接近:
useMemo(() => fn, dependencies)
JavaScript区别在于 API 的表达方式不同。
useMemo:
const value = useMemo(() => {
return calculateSomething();
}, [dependencies]);
JavaScript缓存:
calculateSomething() 的返回值
而 useCallback:
const callback = useCallback(() => {
doSomething();
}, [dependencies]);
JavaScript缓存:
函数引用
因此:
useMemo
↓
关注「值」
useCallback
↓
关注「函数」
这是理解两者区别最简单的方法。
如何判断是否应该使用
在实际项目中,可以按照下面的思路判断。
是否应该使用 useMemo
可以考虑使用:
- 计算明显昂贵。
- 数据量较大。
- 计算会频繁重复。
- 返回对象或数组,并且引用稳定性会影响下游组件。
- Profiler 显示该计算确实存在明显耗时。
通常不需要使用:
- 简单字符串拼接。
- 简单数学运算。
- 小数组的轻量处理。
- 缓存结果不会影响任何性能敏感组件。
是否应该使用 useCallback
可以考虑使用:
- 函数作为 prop 传给
React.memo子组件。 - 函数是其他 Hook 的依赖项。
- 第三方组件依赖 callback 引用稳定。
- 自定义 Hook 明确要求稳定函数引用。
- Profiler 证明函数引用变化导致了明显的额外渲染。
通常不需要使用:
- 函数只在当前组件内部使用。
- 子组件没有做 memoization。
- 子组件渲染成本非常低。
- callback 每次依赖都会变化,导致缓存几乎无法复用。
性能分析比盲目优化更重要
很多时候,开发者看到:
filter
map
sort
function
JavaScript就下意识加入:
useMemo
useCallback
JavaScript这种做法并不一定正确。
更合理的优化流程是:
先实现正确、清晰的代码
↓
使用性能分析工具
↓
定位真实瓶颈
↓
针对问题进行优化
↓
再次测量
↓
确认优化是否有效
React DevTools 的 Profiler 可以查看:
- 哪些组件发生了重新渲染。
- 每次渲染耗时。
- 哪些更新触发了组件渲染。
Chrome DevTools 的 Performance 面板还可以观察:
- JavaScript 执行时间。
- Main Thread 阻塞。
- 长任务。
- 浏览器渲染过程。
- 火焰图。
这些工具通常比盲目增加 useMemo 和 useCallback 更有价值。
React Compiler 对手动记忆化的影响
随着 React 并发特性、Server Components 以及 React Compiler 的发展,React 的性能优化策略也在持续演进。
在启用了 React Compiler 的项目中,编译器可以自动完成许多原本需要开发者通过:
React.memo
useMemo
useCallback
手动实现的记忆化优化。
因此,在使用 React Compiler 的新项目中,不应该机械地为所有值和函数增加 useMemo 或 useCallback。
更合理的做法是综合考虑:
- 项目是否启用了 React Compiler。
- 当前组件是否真的存在重复计算。
- 引用变化是否真的导致昂贵的子组件重新渲染。
- 是否存在需要手动控制引用稳定性的特殊场景。
- Profiler 的实际性能数据。
即使如此,理解 useMemo、useCallback 和 React.memo 仍然非常重要。
因为它们背后反映的是 React 性能优化中的几个核心概念:
引用稳定性
+
依赖关系
+
组件重新渲染
+
计算成本
这些概念依然是理解 React 渲染机制和性能问题的重要基础。
总结
useMemo 和 useCallback 最核心的区别可以概括为:
useMemo
↓
缓存计算结果
↓
减少重复计算
useCallback
↓
缓存函数引用
↓
减少不必要的引用变化
可以记住下面这个判断原则:
不要因为“可以使用”就使用,而应该因为存在明确的性能或引用稳定性需求才使用。
对于 useMemo:
重点看计算成本
对于 useCallback:
重点看函数引用是否影响其他组件或 Hook
而对于任何性能优化:
先测量
再优化
最后再次验证
通常比盲目添加缓存更可靠。