useMemo 和 useCallback 的使用场景

分析 React 性能优化 Hook 的正确使用时机和常见误区

问题

在 React 开发中,useMemouseCallback 经常被过度使用。开发者担心性能问题,给每个组件、函数、变量都套上 memo,导致代码复杂度增加。那么应该在什么场景下使用它们?

解答

三个使用场景

useMemouseCallback 主要用于三个场景:

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 时

关键点

  • 大部分 useMemouseCallback 应该移除,它们可能没有带来优化,反而增加首次渲染负担
  • 优化子组件 re-render 必须同时满足:子组件被 React.memo 缓存,且所有 props 都被缓存
  • 不推荐默认给所有组件使用缓存,会导致过多内存消耗和初始化变慢
  • 组件渲染是性能瓶颈,把优化重点放在渲染昂贵的组件上
  • 当值被 useEffect 依赖时,缓存是必要的