在 React 开发中,性能优化是一个非常重要的环节,尤其是在大型应用中,频繁的渲染和不必要的计算会显著影响用户体验。React 提供了三个非常实用的工具来帮助我们进行性能优化:React.memo、useCallback 和 useMemo。这三者各司其职,合理使用可以显著减少不必要的渲染和计算,提升应用性能。
一、React.memo:避免子组件不必要的渲染
1.作用
React.memo 是一个 高阶组件(Higher-Order Component),用于优化子组件的渲染行为。它通过 浅比较 props 来判断子组件是否需要重新渲染。如果 props 没有变化,就跳过渲染,从而避免不必要的性能开销。
2.使用方式
import React, { memo } from 'react';
const MyComponent = (props) => {
console.log('MyComponent render');
return <div>{props.value}</div>;
};
export default memo(MyComponent);
在这个例子中,MyComponent 会被 React.memo 包裹,只有当 props.value 发生变化时,才会重新渲染。
3.原理:React.memo 的“浅比较”机制
在 React 中,对象和数组是引用类型(reference type),也就是说,它们的比较不是基于值,而是基于内存地址。
例如:
const obj1 = { a: 1, b: 2 };
const obj2 = { a: 1, b: 2 };
console.log(obj1 === obj2); // false
虽然 obj1 和 obj2 的内容完全一样,但由于它们在内存中是两个不同的对象,所以比较结果为 false。
React.memo 默认使用 浅比较(Shallow Compare) 来判断 props 是否发生变化:
- 如果
props是基本类型(如 string、number、boolean),则直接比较值。 - 如果
props是对象或数组,只比较第一层属性的引用地址,不深入嵌套结构。
示例说明
const Child = ({ user }) => {
console.log('Child rendered');
return <div>{user.name}</div>;
};
export default React.memo(Child);
父组件:
const Parent = () => {
const [count, setCount] = useState(0);
const user = { name: 'Alice', age: 25 };
return (
<>
<Child user={user} />
<button onClick={() => setCount(count + 1)}>Increase</button>
</>
);
};
在这个例子中,每次点击按钮都会导致 Parent 重新渲染,user 也会在每次渲染中重新创建,因此 Child 的 props.user 会指向一个新对象,即使内容没有变化,也会触发重新渲染。
如何避免因“浅比较”导致的不必要渲染?
方法一:使用 useMemo 缓存对象(后文会介绍)
const user = useMemo(() => ({ name: 'Alice', age: 25 }), []);
这样,user 只在组件首次渲染时创建一次,后续渲染中引用保持不变。
方法二:使用 useCallback 缓存函数(后文会介绍)
如果你传入的是函数,也要使用 useCallback 缓存函数引用:
const handleClick = useCallback(() => {
console.log('Clicked');
}, []);
方法三:自定义比较函数(高级用法)
如果你不想使用默认的“浅比较”,可以传入一个自定义的比较函数作为 React.memo 的第二个参数:
const areEqual = (prevProps, nextProps) => {
return prevProps.user.name === nextProps.user.name;
};
export default React.memo(Child, areEqual);
在这个例子中,只要 user.name 没有变化,就不会重新渲染 Child。
4.注意事项
- 不要滥用
React.memo,它本身也有性能开销。 - 只有在组件渲染开销较大或
props不常变化时才使用。 - 对于复杂对象或函数类型的
props,建议配合useMemo或useCallback使用。
二、useCallback:缓存函数引用,避免子组件不必要更新
1.作用
useCallback 用于 缓存函数的引用,避免每次组件重新渲染时都创建新的函数实例。这对于避免子组件因函数引用变化而重新渲染非常有用。
2.使用方式
import React, { useCallback } from 'react';
const ParentComponent = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('Clicked', count);
}, [count]);
return <ChildComponent onClick={handleClick} />;
};
在这个例子中,handleClick 函数只有在 count 变化时才会重新生成。
3.原理
- 每次组件重新渲染时,函数组件内部定义的函数都会重新创建。
- 使用
useCallback后,函数引用只有在依赖项发生变化时才会更新。 - 这样可以避免子组件因为函数引用变化而重新渲染。
4.与 React.memo 搭配使用
如果子组件使用了 React.memo,但传入的 props 中包含函数,且该函数每次渲染都不同(未使用 useCallback),那么子组件仍然会重新渲染。因此,useCallback 是 React.memo 的好搭档。
三、useMemo:缓存计算结果,避免重复计算
1.作用
useMemo 用于 缓存计算结果,避免在每次渲染中重复执行昂贵的计算操作。它适用于那些计算复杂、耗时较长的场景。
2.使用方式
import React, { useMemo } from 'react';
const MyComponent = ({ num }) => {
const result = useMemo(() => {
console.log('Computing...');
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += i;
}
return sum * num;
}, [num]);
return <div>{result}</div>;
};
在这个例子中,useMemo 会缓存 result 的值,只有当 num 变化时才会重新计算。
3.原理
useMemo接收一个计算函数和一个依赖项数组。- 只有当依赖项发生变化时,才执行计算函数并更新缓存值。
- 否则返回上一次缓存的结果。
4.使用场景
- 复杂计算(如排序、过滤、格式化等)
- 需要根据状态变化动态生成值
- 避免重复渲染时重复计算
四、实战代码
我们来看一个较为完整的示例,包含了 React.memo、useCallback 和 useMemo ,用来展示如何协同工作的。
1.示例代码
// ParentComponent.jsx
import React, { useState, useCallback, useMemo } from 'react';
import ChildComponent from './ChildComponent';
const ParentComponent = () => {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
// 缓存函数引用
const handleClick = useCallback(() => {
console.log('Button clicked', num);
}, [num]);
// 缓存复杂计算结果
const result = useMemo(() => {
console.log('Expensive calculation');
let res = 0;
for (let i = 0; i < 1000000; i++) {
res += i;
}
return res * num;
}, [num]);
return (
<div>
<div>Count: {count}</div>
<button onClick={() => setCount(count + 1)}>Increase Count</button>
<div>Result: {result}</div>
<button onClick={() => setNum(num + 1)}>Increase Num</button>
<br />
<ChildComponent onClick={handleClick} />
</div>
);
};
export default ParentComponent;
// ChildComponent.jsx
import React, { memo, useEffect } from 'react';
const ChildComponent = ({ onClick }) => {
useEffect(() => {
console.log('ChildComponent mounted');
}, []);
console.log('ChildComponent render');
return <button onClick={onClick}>Click Me</button>;
};
export default memo(ChildComponent);
2.效果分析
| 操作 | 父组件渲染 | 子组件渲染 | 计算执行 |
|---|---|---|---|
| 点击 “Increase Count” | ✅ | ❌(因为 onClick 未变化) | ❌(num 未变化) |
| 点击 “Increase Num” | ✅ | ✅(onClick 引用变化) | ✅(num 变化) |
五、性能优化建议
1. 组件拆分粒度要小
- 拆分成只负责渲染的小组件,便于局部更新。
- 每个组件只关心自己的
props,避免全局状态污染。
2. 避免过度使用 Context
- 所有状态都放在一个 Context 中会导致:
- 更新频繁
- 所有使用该 Context 的组件都会重新渲染
- 建议按业务模块拆分多个 Context。
3. 合理使用 memo + useCallback + useMemo
React.memo控制组件是否重新渲染useCallback控制函数是否重新生成useMemo控制值是否重新计算
4. 避免不必要的依赖项
- 在
useCallback和useMemo中,依赖项越少越好。 - 只添加真正影响结果的依赖项。
5. 使用 Profiler 工具分析性能
- React DevTools 提供了 Profiler 工具,可以分析组件渲染时间、调用栈等。
- 有助于发现性能瓶颈。