useMemo 和 useCallback 的使用场景
分析 React 性能优化 Hook 的正确使用时机和常见误区
问题
在 React 开发中,useMemo 和 useCallback 经常被过度使用。开发者担心性能问题,给每个组件、函数、变量都套上 memo,导致代码复杂度增加。那么应该在什么场景下使用它们?
解答
三个使用场景
useMemo 和 useCallback 主要用于三个场景:
1. 防止不必要的 effect
当值被 useEffect 依赖时,缓存可以避免重复执行 effect:
const Component = () => {
// 在 re-renders 之间缓存 a 的引用
const a = useMemo(() => ({ test: 1 }), []);
useEffect(() => {
// 只有当 a 的值变化时,这里才会被触发
doSomething();
}, [a]);
};
useCallback 同理:
const Component = () => {
const fetch = useCallback(() => {
console.log('fetch some data here');
}, []);
useEffect(() => {
fetch();
}, [fetch]);
};
2. 防止不必要的 re-render
这是最容易误用的场景。React 组件会在三种情况下 re-render:
- 自身的 props 或 state 改变
- Context value 改变
- 父组件重新渲染时,所有子组件都会 re-render
很多开发者忽视第三点,导致无效缓存。例如:
const App = () => {
const [state, setState] = useState(1);
const onClick = useCallback(() => {
console.log('Do something on click');
}, []);
return (
// 无论 onClick 是否被缓存,Page 都会 re-render
<Page onClick={onClick} />
);
};
当 App re-render 时,Page 也会跟着 re-render,这里的 useCallback 完全无效。
要真正防止子组件 re-render,必须同时满足两个条件:
const PageMemoized = React.memo(Page);
const App = () => {
const [state, setState] = useState(1);
const onClick = useCallback(() => {
console.log('Do something on click');
}, []);
return (
<PageMemoized onClick={onClick} />
);
};
但如果添加一个未缓存的 prop,优化就失效了:
const PageMemoized = React.memo(Page);
const App = () => {
const [state, setState] = useState(1);
const onClick = useCallback(() => {
console.log('Do something on click');
}, []);
return (
// Page 仍会 re-render,因为 value 没有被缓存
<PageMemoized onClick={onClick} value={[1, 2, 3]} />
);
};
3. 防止不必要的重复计算
useMemo 可以避免高开销的计算,但实际上高开销的计算极少出现。对 250 个元素的数组排序通常只需要不到 1ms,而组件渲染往往需要几十毫秒。
组件渲染才是性能瓶颈,应该把 useMemo 用在渲染昂贵的组件上,而不是数值计算上。
使用成本
缓存不是免费的,它有三个成本:
- 开发成本:必须保证组件本身和所有 props 都被缓存,后续添加的 props 也要缓存
- 代码复杂度:大量缓存函数降低代码可读性
- 性能成本:组件初始化时的缓存操作会累加,影响首次渲染速度
如何判断组件需要缓存
没有简单的自动化方式,通常通过以下方法:
- 开发或测试过程中人工感知性能问题
- 使用 React Dev Tools Profiler 查看组件性能
- 使用 Profiler API 在自动化测试中检测性能
为什么 React 不默认缓存所有组件
两个原因:
- 缓存有成本,小成本会累加
- 浅比较无法保证正确性,特别是在对象被 mutate 时
关键点
- 大部分
useMemo和useCallback应该移除,它们可能没有带来优化,反而增加首次渲染负担 - 优化子组件 re-render 必须同时满足:子组件被
React.memo缓存,且所有 props 都被缓存 - 不推荐默认给所有组件使用缓存,会导致过多内存消耗和初始化变慢
- 组件渲染是性能瓶颈,把优化重点放在渲染昂贵的组件上
- 当值被
useEffect依赖时,缓存是必要的
目录