在 React 开发中,组件的重新渲染是一个绕不开的话题。默认情况下,当组件的状态(state)或属性(props)发生变化时,组件会重新执行渲染逻辑 ------ 这本身是 React 响应式设计的体现,但如果不加控制,一些无意义的重复计算或渲染会显著影响应用性能。
本文将通过实战案例,详解 React 中两个核心的性能优化 Hook:useMemo 和 useCallback,帮你精准规避不必要的计算和渲染,让组件运行更高效。
一、为什么需要性能优化?先看一个典型问题
先看一段未做优化的代码场景:
javascript
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 [keyword, setKeyword] = useState('');
const list = ['apple', 'banana', 'orange', 'pear'];
// 列表过滤逻辑
const filterList = list.filter(item => {
console.log('filter 执行');
return item.includes(keyword);
});
// 昂贵的计算逻辑
const [num, setNum] = useState(0);
const result = slowSum(num);
return (
<div>
<p>计算结果:{result}</p>
<button onClick={() => setNum(num + 1)}>num+1</button>
<input
type="text"
value={keyword}
onChange={e => setKeyword(e.target.value)}
/>
<button onClick={() => setCount(count + 1)}>count+1</button>
{count}
<ul>
{filterList.map(item => <li key={item}>{item}</li>)}
</ul>
</div>
);
}
这段代码存在两个明显的性能问题:
- 无关状态变更触发重复计算:count 和 keyword 是完全独立的状态,但点击 count+1 时,组件重新渲染,filterList 过滤逻辑和 slowSum 计算都会重新执行;
- 无意义的重复渲染:如果把过滤 / 计算逻辑抽离到子组件,父组件状态变更还会导致子组件无意义的重新渲染。
这些问题在简单场景下不明显,但在复杂计算、长列表渲染的场景中,会直接导致页面卡顿 ------ 而 useMemo 和 useCallback 就是解决这类问题的核心方案。
二、useMemo:缓存计算结果,避免重复执行
useMemo 的核心作用是缓存计算结果,只有当依赖的状态发生变化时,才重新执行计算逻辑;依赖未变时,直接返回缓存的结果。
1. 基本用法
javascript
const 缓存的结果 = useMemo(() => {
// 需要缓存的计算逻辑
return 计算结果;
}, [依赖项数组]);
- 第一个参数:一个函数,封装需要缓存的计算逻辑,函数的返回值就是要缓存的结果;
- 第二个参数:依赖项数组,只有数组中的值发生变化时,才会重新执行第一个参数的函数;
- 核心逻辑:依赖不变 → 复用缓存结果;依赖改变 → 重新计算。
2. 实战优化:缓存过滤 / 昂贵计算
基于前面的问题代码,用 useMemo 优化:
javascript
import { useState, useMemo } 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 [keyword, setKeyword] = useState('');
const list = ['apple', 'banana', 'orange', 'pear'];
// 优化1:缓存列表过滤结果,仅依赖 keyword
const filterList = useMemo(() => {
return list.filter(item => item.includes(keyword));
}, [keyword]);
const [num, setNum] = useState(0);
// 优化2:缓存昂贵计算结果,仅依赖 num
const result = useMemo(() => {
return slowSum(num);
}, [num]);
return (
<div>
<p>计算结果:{result}</p>
<button onClick={() => setNum(num + 1)}>num+1</button>
<input
type="text"
value={keyword}
onChange={e => setKeyword(e.target.value)}
/>
<button onClick={() => setCount(count + 1)}>count+1</button>
{count}
<ul>
{filterList.map(item => <li key={item}>{item}</li>)}
</ul>
</div>
);
}
优化后验证效果:
- 点击 count+1 时,filterList 和 result 都不会重新计算(控制台不会打印「计算中...」和「filter 执行」);
- 只有修改 keyword 时,过滤逻辑才执行;只有点击 num+1 时,slowSum 才重新计算。
3. 注意事项
- useMemo 是性能优化手段,不是语义化工具,不要滥用:简单计算(如 a + b)没必要用 useMemo,反而会增加缓存开销;
- 依赖项数组要准确:漏写依赖会导致缓存结果不更新,多写无关依赖会失去缓存意义;
- useMemo 缓存的是计算结果,适合「有返回值的计算逻辑」(如过滤列表、数值计算)。
三、useCallback:缓存函数,避免子组件无意义渲染
useCallback 常和 memo 配合使用,核心作用是缓存函数引用,避免因父组件重新渲染导致子组件接收的函数 props 频繁变更,从而触发子组件无意义的重新渲染。
1. 先理解 memo:子组件渲染优化
memo 是 React 提供的高阶组件(HOC),作用是浅比较子组件的 props:如果 props 没有变化,子组件就不会重新渲染。
基本用法:
javascript
import { memo } from 'react';
// 用 memo 包裹子组件,实现 props 浅比较
const Child = memo(({ count, handleClick }) => {
console.log('child 重新渲染');
return (
<div onClick={handleClick}>
子组件{count}
</div>
);
});
2. 问题:未缓存的函数会让 memo 失效
如果父组件直接传递一个普通函数给子组件,父组件每次渲染时,这个函数都会被重新创建(引用地址改变)------ 即使 memo 做了 props 比较,也会判定 props 变更,导致子组件重新渲染:
javascript
import { useState, memo } from 'react';
const Child = memo(({ count, handleClick }) => {
console.log('child 重新渲染');
return <div onClick={handleClick}>子组件{count}</div>;
});
export default function App() {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
// 每次父组件渲染,这个函数都会重新创建
const handleClick = () => {
console.log('click');
};
return (
<div>
<button onClick={() => setCount(count + 1)}>count+1</button>
<button onClick={() => setNum(num + 1)}>num+1</button>
{/* 即使 num 变更(和子组件无关),handleClick 引用改变 → 子组件重新渲染 */}
<Child count={count} handleClick={handleClick} />
</div>
);
}
上述代码中,点击 num+1 时,count 没有变化,但 handleClick 函数被重新创建,子组件的 props 判定为变更,因此会打印「child 重新渲染」------ 这就是无意义的渲染。
3. useCallback 解决:缓存函数引用
useCallback 可以缓存函数的引用,只有依赖项变化时,才会重新创建函数:
scss
const 缓存的函数 = useCallback(() => {
// 函数逻辑
}, [依赖项数组]);
用 useCallback 优化上述代码:
javascript
import { useState, memo, useCallback } from 'react';
const Child = memo(({ count, handleClick }) => {
console.log('child 重新渲染');
return <div onClick={handleClick}>子组件{count}</div>;
});
export default function App() {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
// 缓存函数,仅依赖 count
const handleClick = useCallback(() => {
console.log('click');
}, [count]);
return (
<div>
<button onClick={() => setCount(count + 1)}>count+1</button>
<button onClick={() => setNum(num + 1)}>num+1</button>
<Child count={count} handleClick={handleClick} />
</div>
);
}
优化后验证效果:
- 点击 num+1 时,count 未变,handleClick 引用不变 → 子组件 props 无变化 → 不重新渲染;
- 只有点击 count+1 时,依赖项变更,handleClick 重新创建 → 子组件重新渲染(这是必要的渲染)。
4. useCallback 与 useMemo 的区别
很多人会混淆两者,核心区别一句话总结:
- useMemo:缓存函数的返回值,适合「有返回值的计算逻辑」;
- useCallback:缓存函数本身(引用) ,适合「传递给子组件的回调函数」。
简单类比:
scss
// useCallback 等价于 useMemo 包裹函数返回自身
const fn = useCallback(() => {}, []);
// 等同于
const fn = useMemo(() => () => {}, []);
四、核心使用场景总结
| Hook | 核心作用 | 典型使用场景 |
|---|---|---|
| useMemo | 缓存计算结果 | 1. 昂贵的数值计算;2. 长列表过滤 / 排序;3. 复杂数据格式化 |
| useCallback | 缓存函数引用 | 1. 传递给子组件的回调函数;2. 作为其他 Hook 的依赖项 |
五、避坑指南
- 不要过度优化:useMemo/useCallback 本身有缓存开销,简单逻辑(如简单加法、短列表过滤)使用反而会降低性能;
- 依赖项要完整:务必把函数内部用到的所有状态 / 属性都加入依赖数组,否则会导致缓存结果不更新;
- memo 仅浅比较:如果 props 是对象 / 数组(引用类型),即使内容不变,引用改变也会触发子组件渲染,此时需要配合 useMemo 缓存引用类型 props;
- React 18 严格模式:开发环境下组件会渲染两次,useMemo/useCallback 的初始化逻辑也会执行两次,这是正常现象,生产环境不会出现。
总结
useMemo 和 useCallback 是 React 性能优化的「黄金搭档」:
- useMemo 聚焦「计算结果缓存」,解决组件内重复计算的问题;
- useCallback 聚焦「函数引用缓存」,配合 memo 解决子组件无意义渲染的问题。
两者的核心思想都是「缓存」------ 只在依赖项变更时执行必要的逻辑,从而减少不必要的计算和渲染,让 React 应用更流畅。记住:性能优化的前提是「先定位性能瓶颈」,再针对性使用,而非盲目添加优化 Hook。