React 性能调优必备:React.memo、useCallback、useMemo

101 阅读6分钟

在 React 开发中,性能优化是一个非常重要的环节,尤其是在大型应用中,频繁的渲染和不必要的计算会显著影响用户体验。React 提供了三个非常实用的工具来帮助我们进行性能优化:React.memouseCallbackuseMemo。这三者各司其职,合理使用可以显著减少不必要的渲染和计算,提升应用性能。


一、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

虽然 obj1obj2 的内容完全一样,但由于它们在内存中是两个不同的对象,所以比较结果为 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 也会在每次渲染中重新创建,因此 Childprops.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,建议配合 useMemouseCallback 使用。

二、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),那么子组件仍然会重新渲染。因此,useCallbackReact.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.memouseCallbackuseMemo ,用来展示如何协同工作的。

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. 避免不必要的依赖项

  • useCallbackuseMemo 中,依赖项越少越好。
  • 只添加真正影响结果的依赖项。

5. 使用 Profiler 工具分析性能

  • React DevTools 提供了 Profiler 工具,可以分析组件渲染时间、调用栈等。
  • 有助于发现性能瓶颈。