React性能优化:深入理解useMemo和useCallback
前言
在React函数组件中,每次状态更新都会导致整个组件函数重新执行。如果组件内部有复杂的计算或者传递了回调函数给子组件,可能会引发不必要的性能开销。React为我们提供了两个重要的Hook:useMemo和useCallback,用于缓存计算结果和函数引用,从而优化组件性能。本文将从浅入深,结合实际代码,带你彻底理解这两个Hook的使用场景和原理。
1. 性能优化的必要性
我们先看一个简单的例子:
jsx
function App() {
const [count, setCount] = useState(0);
const [keyword, setKeyword] = useState('');
const list = ['apple', 'banana', 'orange', 'pear'];
// 每次渲染都会重新执行filter
const filterList = list.filter(item => {
// 测试fliter是否执行
console.log("filter执行了")
return item.includes(keyword)
});
return (
<div>
<input value={keyword} onChange={e => setKeyword(e.target.value)} />
<button onClick={() => setCount(count + 1)}>count: {count}</button>
<ul>
{filterList.map(item => <li key={item}>{item}</li>)}
</ul>
</div>
);
}
这里 filterList 依赖于 keyword,但每次 count 改变导致组件重新渲染时,filterList 也会重新计算。如果 list 很大或者过滤逻辑复杂,这种不必要的计算就会影响性能。同样,如果我们将一个函数作为 prop 传递给子组件,每次父组件渲染都会生成一个新函数,导致子组件(即使使用 React.memo)也无法避免重新渲染。这正是 useMemo 和 useCallback 要解决的问题。
效果图
2. useMemo:缓存计算结果
useMemo 用于缓存一个计算后的值,只有当依赖项发生变化时,才会重新计算。
2.1 基本用法
jsx
const cachedValue = useMemo(computeFn, dependencies);
computeFn:纯函数,返回需要缓存的值。dependencies:依赖项数组,当任意依赖项变化时,重新执行computeFn。
2.2 优化列表过滤
改进上面的例子:
jsx
import { useState, useMemo } from 'react';
export default function App() {
const [count, setCount] = useState(0);
const [keyword, setKeyword] = useState('');
const list = ['apple', 'banana', 'orange', 'pear'];
// 只有 keyword 变化时才重新过滤
const filterList = useMemo(() => {
console.log('filter执行了'); // 依赖变化时打印
return list.filter(item => item.includes(keyword));
}, [keyword]);
return (
<div>
<input value={keyword} onChange={e => setKeyword(e.target.value)} />
<button onClick={() => setCount(count + 1)}>count: {count}</button>
<ul>
{filterList.map(item => <li key={item}>{item}</li>)}
</ul>
</div>
);
}
现在,点击 count 按钮不会触发 filter 重新执行,只有输入框改变时才会。
效果图

2.3 缓存昂贵计算
假设有一个非常耗时的函数 slowSum:
jsx
function slowSum(n) {
console.log('计算中...');
let sum = 0;
for (let i = 0; i < n * 1000000; i++) {
sum += i;
}
return sum;
}
我们可以用 useMemo 将其结果缓存起来:
jsx
const [num, setNum] = useState(0);
const result = useMemo(() => slowSum(num), [num]);
这样只有当 num 变化时才会重新计算,其他状态变化不会触发 slowSum,这样做我们就可以节约成本。
2.4 小结
useMemo适用于需要缓存计算结果的场景,比如过滤、排序、复杂数学运算等。- 依赖数组要正确填写,避免遗漏或多余依赖。
- 不要滥用,如果计算本身不昂贵,就没有必要缓存。
3. useCallback:缓存函数引用
在React中,函数组件每次渲染都会重新创建内部定义的函数。如果这个函数作为 prop 传递给子组件,即使子组件使用了 React.memo,也会因为每次父组件传递的函数引用不同而导致子组件重新渲染。useCallback 就是用来缓存函数引用的。
3.1 基本用法
jsx
const cachedFn = useCallback(fn, dependencies);
fn:需要缓存的函数。dependencies:依赖项数组,当依赖变化时,重新创建函数。
3.2 配合 React.memo 优化子组件
先看一个没有优化的例子:
jsx
import { useState, memo } from 'react';
const Child = memo(({ count, handleClick }) => {
console.log('子组件渲染了');
return <div onClick={handleClick}>子组件 count: {count}</div>;
});
export default function App() {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
// 每次 App 渲染,这里都会生成一个新函数
const handleClick = () => {
console.log('点击了');
};
return (
<div>
<button onClick={() => setCount(count + 1)}>count+1</button>
<button onClick={() => setNum(num + 1)}>num+1</button>
<Child count={count} handleClick={handleClick} />
</div>
);
}
虽然 Child 用 memo 包裹,理论上只有 count 或 handleClick 变化时才会重新渲染。但由于 handleClick 每次都是新函数,memo 浅比较发现 handleClick 引用变了,导致子组件也会重新渲染。即使点击的是 num 按钮,子组件依然会渲染。
效果图,可以看到当我们点击nums组件时,我们的子组件依然会执行

使用 useCallback 缓存 handleClick:
jsx
const handleClick = useCallback(() => {
console.log('点击了');
}, []); // 空依赖表示函数永远不会重新创建
现在点击 num 按钮,handleClick 引用不变,子组件就不会重新渲染。
效果图,可以看到这个适合当我们点击nums组件,子组件就不会执行了

3.3 依赖项的作用
如果回调函数内部使用了某些状态,需要将这些状态添加到依赖数组中,否则函数内部会一直使用旧的闭包值。
jsx
const handleClick = useCallback(() => {
console.log('当前count:', count);
}, [count]); // 当 count 变化时,重新生成函数
这样既能保证函数引用在 count 不变时稳定,又能在 count 变化时获取最新值。
3.4 小结
useCallback主要用于将回调函数传递给经过memo优化的子组件,避免不必要的重绘。- 如果回调函数不依赖任何组件状态,依赖数组可以为空。
- 注意不要过度优化,如果子组件很轻量或者渲染成本很低,不一定需要使用
useCallback。
4. useMemo 与 useCallback 的关系
useMemo缓存的是值 ,useCallback缓存的是函数。- 实际上,
useCallback(fn, deps)等价于useMemo(() => fn, deps)。 - 两者都是通过闭包和依赖追踪来实现缓存,目的都是减少不必要的计算或渲染。
5. 常见误区与注意事项
5.1 依赖数组要完整
无论是 useMemo 还是 useCallback,都要确保依赖数组中包含了所有在回调/计算中使用的响应式值(props、state、context等)。否则会因闭包捕获旧值而产生 bug。可以使用 eslint-plugin-react-hooks 自动检查依赖。
5.2 不要过早优化
只有在确实存在性能瓶颈时才使用这两个 Hook。滥用会增加代码复杂度,且缓存本身也有开销。可以通过 React DevTools 的 Profiler 来识别需要优化的组件。
5.3 缓存稳定但非"纯"的函数
如果 useCallback 的回调函数依赖于外部状态,但依赖数组为空,则函数内部的变量会一直保持初始值,可能导致 bug。一定要根据实际依赖填写数组。
6. 总结
useMemo用于缓存计算值,避免每次渲染都重新执行昂贵计算。useCallback用于缓存函数引用,配合React.memo避免子组件不必要的重新渲染。- 两者都需要指定依赖项,确保缓存内容在依赖变化时更新。
- 性能优化应当有的放矢,先测量后优化,不要盲目使用。
通过合理使用这两个 Hook,我们可以让 React 应用在复杂场景下依然保持流畅。希望本文能帮助你深入理解 useMemo 和 useCallback,并在实际项目中正确应用它们。
本文代码示例基于 React 18,你可以在自己的项目中尝试并观察效果。如果有任何疑问或见解,欢迎在评论区讨论!
