React 性能优化必看:useMemo 与 useCallback 实战解析(附完整代码)
作为 React 开发者,你是否遇到过这样的问题:组件明明只改了一个无关状态,却触发了不必要的重新渲染、昂贵的计算重复执行,导致页面卡顿?
其实这不是 React 的"bug",而是函数组件的默认行为------只要组件的状态(state)或属性(props)发生改变,整个组件函数就会重新执行一遍。
而 useMemo 和 useCallback,就是 React 官方提供的两个"性能优化利器",专门解决这类问题。今天结合具体代码案例,从"痛点→解决方案→实战用法",带你彻底搞懂这两个 Hook 的用法,再也不用为组件性能焦虑!
一、先搞懂:为什么需要 useMemo 和 useCallback?
在讲用法之前,我们先明确核心痛点------不必要的计算和不必要的组件重渲染,这也是我们优化的核心目标。
痛点1:无关状态改变,触发昂贵计算重复执行
先看一段未优化的代码(简化版):
ini
import { useState } from 'react';
// 模拟昂贵的计算(比如大数据量处理、复杂运算)
function slowSum(n) {
console.log('计算中...'); // 用于观察是否重复执行
let sum = 0;
for (let i = 0; i < n * 10000000; i++) {
sum += i;
}
return sum;
}
export default function App() {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
// 昂贵的计算,依赖 num
const result = slowSum(num);
return (
计算结果:{result}<button onClick={ setNum(num + 1)}>num+1(触发计算)无关状态:{count}<button onClick={ setCount(count + 1)}>count+1(无关操作)
);
}
运行后你会发现:点击「count+1」(改变和计算无关的状态),控制台依然会打印「计算中...」------这意味着,即使计算依赖的 num 没有变,昂贵的 slowSum 函数也会重新执行。
这就是典型的"无效计算",当计算足够复杂时,会明显拖慢页面性能。
痛点2:无关状态改变,触发子组件重复渲染
React 中,父组件重新渲染时,默认会带动所有子组件一起重新渲染。即使子组件的 props 没有任何变化,也会"无辜躺枪"。
再看一段未优化的代码:
ini
import { useState } from 'react';
// 子组件:仅展示 count 和触发点击事件
const Child = ({ count, handleClick }) => {
console.log('子组件重新渲染'); // 观察重渲染情况
return (
<div onClick={子组件:{count}
);
};
export default function App() {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
// 父组件传递给子组件的回调函数
const handleClick = () => {
console.log('点击子组件');
};
return (
<button onClick={ setCount(count + 1)}>count+1(关联子组件)<button onClick={ setNum(num + 1)}>num+1(无关子组件)<Child count={count} handleClick={handleClick} />
);
}
运行后发现:点击「num+1」(改变和子组件无关的状态),控制台依然会打印「子组件重新渲染」。
原因很简单:父组件重新执行时,会重新生成一个新的 handleClick 函数(即使函数逻辑没变),而子组件的 props 包含这个新函数,React 会认为"props 变了",从而触发子组件重渲染。
而这两个痛点,正好可以用 useMemo 和 useCallback 分别解决------useMemo 缓存计算结果,useCallback 缓存回调函数。
二、useMemo:缓存计算结果,避免无效计算
1. 核心作用
useMemo(Memo = Memoization,记忆化)的核心功能是:缓存"昂贵计算"的结果,只有当依赖项发生改变时,才重新执行计算;依赖项不变时,直接返回缓存的结果。
相当于 Vue 中的 computed 计算属性,专门用于处理"依赖某个/某些状态、需要重复执行的计算逻辑"。
2. 语法格式
javascript
const 缓存的结果 = useMemo(() => {
// 这里写需要缓存的计算逻辑
return 计算结果;
}, [依赖项数组]);
参数说明:
- 第一个参数:函数,封装需要缓存的计算逻辑,函数的返回值就是要缓存的结果。
- 第二个参数:依赖项数组,只有当数组中的依赖项发生改变时,才会重新执行第一个参数的函数,重新计算结果;否则直接返回缓存值。
3. 实战优化:解决"无效计算"问题
我们用 useMemo 优化前面的"昂贵计算"案例:
scss
import { useState, useMemo } from 'react'; // 导入 useMemo
// 模拟昂贵的计算
function slowSum(n) {
console.log('计算中...');
let sum = 0;
for (let i = 0; i< n * 10000000; i++) {
sum += i;
}
return sum;
}
export default function App() {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
// 用 useMemo 缓存计算结果,依赖项只有 num
const result = useMemo(() => {
return slowSum(num); // 计算逻辑封装在函数中
}, [num]); // 只有 num 改变时,才重新计算
return (
计算结果:{result}<button onClick={ setNum(num + 1)}>num+1(触发计算)无关状态:{count}<button onClick={ setCount(count + 1)}>count+1(无关操作)
);
}
优化后效果:
- 点击「num+1」:num 改变,依赖项变化,重新执行 slowSum,打印「计算中...」;
- 点击「count+1」:count 改变,但 num 未变,依赖项不变,直接返回缓存的 result,不再执行 slowSum,控制台无打印。
4. 补充案例:缓存列表过滤结果
除了昂贵计算,列表过滤、数据处理等场景也适合用 useMemo。比如下面的列表过滤案例:
ini
import { useState, useMemo } from 'react';
export default function App() {
const [count, setCount] = useState(0);
const [keyword, setKeyword] = useState('');
const list = ['apple', 'banana', 'orange', 'pear'];
// 用 useMemo 缓存过滤结果,依赖项只有 keyword
const filterList = useMemo(() => {
console.log('过滤执行');
return list.filter(item => item.includes(keyword));
}, [keyword]); // 只有 keyword 改变时,才重新过滤
return (
<input
type="text"
value={ setKeyword(e.target.value)}
placeholder="搜索水果"
/>
无关状态:{count}<button onClick={ setCount(count + 1)}>count+1
{filterList.map(item => (<li key={{item}
))}
);
}
优化后:只有输入关键词(keyword 改变)时,才会重新执行过滤;点击 count+1 时,过滤逻辑不会重复执行,提升组件性能。
5. 注意点
- 不要滥用 useMemo:如果计算逻辑很简单(比如
count * 2),使用 useMemo 反而会增加缓存的开销,得不偿失; - 依赖项数组不能漏:如果计算逻辑依赖某个状态,但没写进依赖数组,useMemo 会一直返回初始缓存值,导致数据不一致;
- useMemo 缓存的是"计算结果",不是函数本身。
三、useCallback:缓存回调函数,避免子组件无效重渲染
1. 核心作用
useCallback 的核心功能是:缓存回调函数本身,避免父组件重新渲染时,频繁生成新的函数实例,从而防止子组件因 props 变化而无效重渲染。
它常和 memo(高阶组件)配合使用------memo 用于优化子组件,避免子组件在 props 未变时重渲染;useCallback 用于缓存传递给子组件的回调函数,确保函数实例不变。
2. 先认识 memo
在讲 useCallback 之前,必须先了解 memo:
- memo 是 React 提供的高阶组件(HOC),接收一个函数组件作为参数,返回一个"优化后的新组件";
- 作用:对比子组件的前后 props,如果 props 没有变化,就阻止子组件重新渲染;
- 局限性:只能浅对比 props(基本类型对比值,引用类型对比地址),如果传递的是函数、对象,memo 会认为"地址变了,props 变了",依然会触发重渲染。
3. 语法格式
scss
const 缓存的回调函数 = useCallback(() => {
// 这里写回调函数的逻辑
}, [依赖项数组]);
参数说明和 useMemo 一致:
- 第一个参数:需要缓存的回调函数;
- 第二个参数:依赖项数组,只有依赖项改变时,才会生成新的函数实例;否则返回缓存的函数实例。
4. 实战优化:解决"子组件无效重渲染"问题
用 useCallback + memo 优化前面的子组件重渲染案例:
scss
import { useState, memo, useCallback } from 'react'; // 导入 memo 和 useCallback
// 用 memo 包装子组件,优化重渲染
const Child = memo(({ count, handleClick }) => {
console.log('子组件重新渲染');
return (
<div onClick={子组件:{count}
);
});
export default function App() {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
// 用 useCallback 缓存回调函数,依赖项只有 count
const handleClick = useCallback(() => {
console.log('点击子组件');
}, [count]); // 只有 count 改变时,才生成新的函数实例
return (
<button onClick={ setCount(count + 1)}>count+1(关联子组件)<button onClick={ => setNum(num + 1)}>num+1(无关子组件)<Child count={count} handleClick={handleClick} />
);
}
优化后效果:
- 点击「count+1」:count 改变,handleClick 的依赖项变化,生成新的函数实例,子组件 props 改变,触发重渲染;
- 点击「num+1」:num 改变,父组件重新执行,但 handleClick 依赖项(count)未变,返回缓存的函数实例,子组件 props 未变,不触发重渲染。
5. 注意点
- useCallback 必须和 memo 配合使用:如果子组件没有用 memo 包装,即使缓存了回调函数,子组件依然会跟随父组件重渲染;
- 依赖项数组要准确:如果回调函数中用到了父组件的状态/属性,必须写进依赖项数组,否则会出现"闭包陷阱"(拿到旧的状态值);
- useCallback 缓存的是"函数实例",不是函数的执行结果(和 useMemo 本质区别)。
四、useMemo 与 useCallback 核心区别(必记)
很多人会混淆这两个 Hook,用一张表快速区分:
| Hook | 核心功能 | 缓存内容 | 使用场景 |
|---|---|---|---|
| useMemo | 优化计算逻辑,避免无效计算 | 计算结果(值) | 昂贵计算、列表过滤、数据处理 |
| useCallback | 优化子组件重渲染,避免无效渲染 | 回调函数(函数实例) | 父组件向子组件传递回调函数 |
一句话总结:useMemo 缓存"值",useCallback 缓存"函数" ,两者都是为了减少不必要的执行,提升 React 组件性能。
五、实战避坑指南
1. 不要盲目优化
React 本身的渲染性能已经很好,对于简单组件、简单计算,无需使用 useMemo 和 useCallback------缓存本身也需要消耗内存,过度优化反而会增加性能负担。
建议:只有当你明确遇到"计算卡顿""子组件频繁重渲染"时,再进行优化。
2. 依赖项数组不能乱填
- 不要空数组:空数组表示"永远不更新",如果计算/函数依赖某个状态,会导致数据不一致;
- 不要漏填依赖:如果计算/函数中用到了某个状态/属性,必须写进依赖项数组;
- 不要多填依赖:无关的依赖会导致不必要的重新计算/函数更新。
3. 配合其他优化手段
useMemo 和 useCallback 不是唯一的性能优化方式,还可以配合:
- memo:优化子组件重渲染;
- useEffect 清理函数:避免内存泄漏;
- 拆分组件:将复杂组件拆分为多个小组件,减少重渲染范围。
六、总结
useMemo 和 useCallback 是 React 性能优化的"黄金搭档",核心都是通过"缓存"减少不必要的执行:
- 当有昂贵计算,且计算依赖特定状态时,用 useMemo 缓存计算结果;
- 当需要向子组件传递回调函数,且希望避免子组件无效重渲染时,用 useCallback 缓存函数实例,配合 memo 使用。
记住:性能优化的核心是"解决实际问题",而不是盲目使用 API。先定位性能瓶颈,再选择合适的优化方式,才能写出高效、流畅的 React 组件。
最后,把文中的代码复制到本地,亲自运行一遍,感受优化前后的差异,你会对这两个 Hook 有更深刻的理解