React性能优化:useMemo vs useCallback
在React中,useMemo和useCallback都是用于性能优化的Hook,核心作用是避免不必要的计算或重渲染,但它们的应用场景和作用对象不同。
一、基本用法与场景
1. useMemo:缓存计算结果
useMemo用于缓存计算密集型操作的结果,避免在每次渲染时重复执行昂贵的计算。
语法:
const memoizedValue = useMemo(() => {// 复杂计算逻辑(如大量数据处理、循环计算等)return 计算结果;
}, [依赖数组]); // 当依赖项变化时,才重新计算
适用场景:
当组件渲染时需要执行耗时的计算(如大数据过滤、排序、复杂公式计算等),且计算结果仅依赖特定变量时,用useMemo缓存结果,避免每次渲染都重复计算。
示例:
function ProductList({ products, filterText }) {// 过滤商品列表(假设products数据量大,过滤逻辑复杂)const filteredProducts = useMemo(() => {return products.filter(product => product.name.includes(filterText));}, [products, filterText]); // 仅当products或filterText变化时,才重新过滤return (<ul>{filteredProducts.map(product => (<li key={product.id}>{product.name}</li>))}</ul>);
}
2. useCallback:缓存函数引用
useCallback用于缓存函数的引用,避免每次渲染时创建新的函数实例(即使函数逻辑没变)。
语法:
const memoizedCallback = useCallback(() => {// 函数逻辑
}, [依赖数组]); // 当依赖项变化时,才创建新的函数引用
适用场景:
当需要将函数作为props传递给子组件,且子组件通过React.memo(或PureComponent)优化时,用useCallback缓存函数引用,避免子组件因“函数引用变化”而触发不必要的重渲染。
示例:
// 子组件(用React.memo优化,仅在props真正变化时重渲染)
const Child = React.memo(({ onHandleClick }) => {console.log("Child 渲染了");return <button onClick={onHandleClick}>点击</button>;
});// 父组件
function Parent() {const [count, setCount] = useState(0);// 用useCallback缓存函数引用:仅当count变化时,才会创建新函数const handleClick = useCallback(() => {console.log("点击了,当前count:", count);}, [count]); // 依赖countreturn (<div><p>count: {count}</p><button onClick={() => setCount(count + 1)}>加1</button><Child onHandleClick={handleClick} /></div>);
}
如果不用useCallback,每次Parent渲染时会创建新的handleClick函数,Child会误以为props变化而重新渲染(即使React.memo优化也无效);用useCallback后,只有count变化时handleClick引用才变,Child才会重新渲染。
二、核心区别
| 维度 | useMemo | useCallback |
|---|---|---|
| 缓存对象 | 计算结果(值) | 函数引用 |
| 作用 | 避免重复执行昂贵计算 | 避免子组件因函数引用变化重渲染 |
| 本质 | useMemo(fn, deps) 等价于 useCallback(fn, deps) 的返回值是 fn 本身,而 useMemo 的返回值是 fn() 的结果 |
三、滥用的后果
useMemo和useCallback并非“越多越好”,滥用会导致负面影响:
-
增加内存开销:
缓存需要占用内存存储计算结果或函数引用,对于简单计算或不常变化的场景,缓存的内存成本可能超过优化收益。 -
逻辑错误(过时闭包):
若依赖数组设置不当(如遗漏依赖),会导致缓存的结果/函数“过时”(引用旧的变量值),引发难以调试的bug。
例:依赖数组遗漏变量,导致函数始终使用初始值:const [count, setCount] = useState(0); // 错误:依赖数组遗漏count,handleClick始终引用初始count(0) const handleClick = useCallback(() => {console.log(count); // 永远输出0 }, []); -
代码复杂度提升:
过度使用会让代码冗余(额外的Hook调用和依赖管理),降低可读性和可维护性。实际上,大多数简单场景(如轻量计算、不传递给子组件的函数)不需要缓存。 -
抵消优化效果:
对于极简单的计算(如a + b),useMemo的缓存逻辑本身(判断依赖变化、读取缓存)可能比直接计算更耗时。
总结
useMemo:缓存计算结果,用于优化“昂贵计算”的重复执行。useCallback:缓存函数引用,用于优化“子组件因函数props变化的重渲染”。- 原则:只在明确存在性能问题时使用(如频繁重渲染导致卡顿、计算耗时过长),避免过早优化和滥用。
