React 性能优化:memo 与 PureComponent

深入理解 React.memo 的实现原理(浅比较 props)、PureComponent 的 shouldComponentUpdate 自动浅比较、自定义比较函数,以及 memo 与 useMemo 的区别和避免过早优化的代价。

React 性能优化:memo 与 PureComponent

为什么性能优化是 React 开发者的必修课

React 的声明式模型使得 UI 开发变得简单:你描述 UI 应该是什么样子,React 负责更新实际的 DOM。但这种便利性是有代价的——如果不小心,很容易写出不必要的渲染,导致应用变慢。

理解 React 的渲染机制和性能优化工具,是构建高性能 React 应用的必要条件。这不仅仅是"让应用变快"的问题,更是理解 React 工作原理的体现。

面试定位:性能优化是 React 面试中高阶话题。面试官通过候选人对 React.memo 和 PureComponent 的理解深度、是否能识别"不值得 memo 的场景"、是否能解释 memo 与 useMemo 的区别,来评估其对 React 渲染模型的理解和性能优化的工程判断能力。


一、React.memo 的实现原理:浅比较 props

1.1 什么是 React.memo

React.memo 是一个高阶组件(HOC),它包装一个组件,使其仅在 props 变化时重新渲染

const MemoizedComponent = React.memo(function MyComponent(props) {
  return <div>{props.name}</div>;
});

React.memo 的第二个参数是一个自定义比较函数:

const MemoizedComponent = React.memo(
  function MyComponent(props) {
    return <div>{props.name}</div>;
  },
  (prevProps, nextProps) => {
    // 返回 true 表示 props 相等,不重新渲染
    // 返回 false 表示 props 不相等,需要重新渲染
    return prevProps.name === nextProps.name;
  }
);

1.2 浅比较(Shallow Comparison)机制

React.memo 默认使用浅比较来判断 props 是否变化:

// React.memo 内部的比较逻辑(伪代码)
function shallowCompare(prevProps, nextProps) {
  const keys = Object.keys({ ...prevProps, ...nextProps });

  for (const key of keys) {
    if (prevProps[key] !== nextProps[key]) {
      return false; // 有变化,需要重新渲染
    }
  }
  return true; // 没有变化,跳过渲染
}

浅比较的问题:对于引用类型的 props(对象、数组、函数),浅比较只比较引用,不比较内容。

function Parent() {
  const [count, setCount] = useState(0);

  return (
    // 每次 count 变化,renderItem 都会被重新渲染
    // 因为 style 对象每次都是新引用
    <MemoizedList
      items={['a', 'b', 'c']}
      style={{ margin: 16 }} // 每次渲染都是新对象 {}
    />
  );
}

1.3 React.memo 的缓存策略

React.memo 缓存的是组件的渲染结果,而不是组件函数本身。当 props 没有变化时,React 直接复用上次渲染的结果,不需要再次执行组件函数。

const MemoizedComponent = React.memo(function Component({ name, age }) {
  // 只有 name 或 age 变化时,这段代码才会重新执行
  console.log('Component rendered');
  return <div>{name}: {age}</div>;
});

二、什么时候应该用 React.memo

2.1 值得使用 memo 的场景

场景一:组件渲染频繁,但 props 变化少

// 一个大型列表中的每个列表项
function ListItem({ item, onSelect }) {
  // 只有 item 变化或 onSelect 变化时才需要重新渲染
  return (
    <div onClick={() => onSelect(item.id)}>
      <span>{item.name}</span>
    </div>
  );
}

const MemoizedListItem = React.memo(ListItem);

场景二:组件树深层嵌套,父组件频繁更新

function App() {
  const [theme, setTheme] = useState('light');

  return (
    <div className={theme}>
      <Header />  {/* Header 不需要重新渲染 */}
      <Sidebar />  {/* Sidebar 不需要重新渲染 */}
      <Content onThemeChange={setTheme} /> {/* 只有 Content 需要 */}
    </div>
  );
}

场景三:组件有重计算逻辑

const ExpensiveChart = React.memo(function ExpensiveChart({ data }) {
  // 数据没有变化时,跳过复杂的图表重绘
  const processedData = processData(data); // 耗时计算
  return <Chart data={processedData} />;
});

2.2 不值得使用 memo 的场景

场景一:组件本身渲染很快

// 一个简单的按钮组件,渲染时间 < 1ms
// memo 的比较开销可能比节省的渲染时间还大
function SimpleButton({ label, onClick }) {
  return <button onClick={onClick}>{label}</button>;
}

// 不要 memo 化这个组件——不值得

场景二:Props 每次都创建新引用

function Parent() {
  return (
    <>
      {/* 每次渲染都创建新对象,memo 永远比较失败 */}
      <Child style={{ color: 'red' }} />

      {/* 每次渲染都创建新函数,memo 永远比较失败 */}
      <Child onClick={() => doSomething()} />
    </>
  );
}

// 这种情况下 memo 化毫无意义,反而增加了比较开销

场景三:组件使用 Context,且 Context 值经常变化

const ThemeContext = createContext('light');

function App() {
  const [theme, setTheme] = useState('light');

  return (
    <ThemeContext.Provider value={theme}>
      <Toolbar /> {/* theme 变化时整个子树都重渲染 */}
    </ThemeContext.Provider>
  );
}

在这种情况下,memo 化 Toolbar 的子组件没有意义,因为 Context 变化会导致它们重新渲染。

2.3 性能测试的重要性

过早优化是万恶之源。在应用 memo 之前,应该先测量

// 使用 React DevTools Profiler 测量
// 或使用 useEffect 测量渲染时间
function useRenderTimer(name) {
  const lastRender = useRef(Date.now());
  const renderCount = useRef(0);

  useEffect(() => {
    const now = Date.now();
    const duration = now - lastRender.current;
    if (duration > 16) { // 超过一帧
      console.log(`${name} 渲染耗时: ${duration}ms`);
    }
    lastRender.current = now;
    renderCount.current++;
  });

  return renderCount.current;
}

三、PureComponent 的 shouldComponentUpdate

3.1 什么是 PureComponent

PureComponent 是 React 提供的类组件基类,它自动对 props 和 state 进行浅比较,只有当它们确实发生变化时才重新渲染。

class UserList extends React.PureComponent {
  render() {
    // 只有 this.props.users 变化时才重新执行
    return (
      <ul>
        {this.props.users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    );
  }
}

3.2 PureComponent vs Component

// Component:每次 props 变化或 setState 调用都会重新渲染
class UserList extends React.Component {
  render() {
    return <div>{this.props.title}</div>;
  }
}

// PureComponent:自动浅比较 props 和 state
class UserList extends React.PureComponent {
  render() {
    return <div>{this.props.title}</div>;
  }
}

PureComponent 的 shouldComponentUpdate 自动做的是

// PureComponent 等价于手动实现:
class UserList extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    // 浅比较 props
    const propsKeys = Object.keys(this.props);
    for (const key of propsKeys) {
      if (this.props[key] !== nextProps[key]) {
        return true;
      }
    }
    // 浅比较 state
    const stateKeys = Object.keys(this.state);
    for (const key of stateKeys) {
      if (this.state[key] !== nextState[key]) {
        return true;
      }
    }
    return false;
  }

  render() {
    return <div>{this.props.title}</div>;
  }
}

3.3 函数组件的 memo vs 类组件的 PureComponent

React.memo 在函数组件上做的事情,本质上与 PureComponent 在类组件上做的事情相同:浅比较 props,决定是否跳过渲染

// 类组件
class UserList extends React.PureComponent {
  render() { /* ... */ }
}

// 函数组件(等价)
const UserList = React.memo(function UserList({ users }) {
  return /* ... */;
});

选择建议

  • 新代码:优先使用函数组件 + React.memo
  • 遗留代码:类组件可以用 PureComponent 替代 Component

四、自定义比较函数(第二个参数)

4.1 为什么需要自定义比较

默认的浅比较在某些场景下不够用:

// 默认比较无法检测对象内容的深层变化
const UserCard = React.memo(
  function UserCard({ user }) {
    return <div>{user.name} - {user.profile.bio}</div>;
  },
  (prevProps, nextProps) => {
    // 自定义比较逻辑
    return (
      prevProps.user.id === nextProps.user.id &&
      prevProps.user.name === nextProps.user.name
    );
  }
);

4.2 自定义比较的场景

场景一:只关心特定 props

const ProductCard = React.memo(
  function ProductCard({ product, onAddToCart }) {
    // 只关心 product 变化,不关心 onAddToCart
    return (
      <div>
        <h3>{product.name}</h3>
        <button onClick={onAddToCart}>添加</button>
      </div>
    );
  },
  (prevProps, nextProps) => {
    return prevProps.product.id === nextProps.product.id;
  }
);

场景二:复杂的比较逻辑

const DataTable = React.memo(
  function DataTable({ columns, data, sortConfig }) {
    return /* ... */;
  },
  (prevProps, nextProps) => {
    // 检查关键 props 是否相等
    return (
      arraysEqual(prevProps.columns, nextProps.columns) &&
      arraysEqual(prevProps.data, nextProps.data) &&
      prevProps.sortConfig.key === nextProps.sortConfig.key &&
      prevProps.sortConfig.direction === nextProps.sortConfig.direction
    );
  }
);

4.3 useMemo 在比较函数中的应用

有时候,你希望 memo 的 props 本身是稳定的引用——这时可以用 useMemo

function Parent() {
  const [count, setCount] = useState(0);

  // columns 数组只有在 data 变化时才重新创建
  const columns = useMemo(() => [
    { key: 'name', label: '名称' },
    { key: 'email', label: '邮箱' },
  ], []); // 空依赖,永久缓存

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>
        Count: {count}
      </button>
      <DataTable columns={columns} data={data} />
      {/* count 变化不会导致 columns 重新创建 */}
      {/* DataTable 的 memo 比较会通过 */}
    </>
  );
}

五、memo 与 useMemo 的区别

5.1 功能定位不同

维度 React.memo useMemo
本质 HOC(包装组件) Hook(缓存值)
缓存内容 组件的渲染结果 任意计算结果
触发重新渲染条件 props 变化 依赖数组中的值变化
使用场景 组件级别的渲染优化 值的计算缓存

5.2 React.memo 缓存组件渲染

const MemoizedComponent = React.memo(function Component({ data }) {
  // 只有 data 引用变化时才重新渲染
  return <ExpensiveRender data={data} />;
});

5.3 useMemo 缓存计算结果

function Component({ items, filter }) {
  // 只有 items 或 filter 变化时才重新计算
  const filteredItems = useMemo(() => {
    return items.filter(item => item.name.includes(filter));
  }, [items, filter]);

  return <List items={filteredItems} />;
}

5.4 两者的配合使用

function ProductList({ products, category }) {
  // useMemo 缓存过滤后的结果
  const filteredProducts = useMemo(() => {
    return products.filter(p => p.category === category);
  }, [products, category]);

  // React.memo 缓存列表项组件的渲染结果
  return (
    <div>
      {filteredProducts.map(product => (
        <MemoizedProductCard
          key={product.id}
          product={product}
        />
      ))}
    </div>
  );
}

const MemoizedProductCard = React.memo(function ProductCard({ product }) {
  return (
    <div>
      <h3>{product.name}</h3>
      <p>{product.price}</p>
    </div>
  );
});

5.5 常见错误:混淆使用

// 错误:useMemo 用于组件
function Component({ data }) {
  const MemoizedComponent = useMemo(
    () => <ExpensiveComponent data={data} />,
    [data]
  );
  return MemoizedComponent; // 返回的是 JSX,不是组件
}

// 正确:React.memo 用于组件
const MemoizedComponent = React.memo(function ExpensiveComponent({ data }) {
  return <div>{data}</div>;
});

function Component({ data }) {
  return <MemoizedComponent data={data} />;
}

六、避免不必要的 memo:过早优化的代价

6.1 memo 不是免费的

memo 有以下成本:

  1. 比较开销:每次渲染都需要比较 props,对于简单组件可能比渲染本身还贵
  2. 内存开销:需要存储上一次的渲染结果
  3. 代码复杂性:增加代码理解的难度
// 一个简单组件
function SimpleDiv({ text }) {
  return <div>{text}</div>;
}

// memo 化后的比较成本
// 每次渲染:Object.keys(props) + 浅比较
// 简单组件的渲染成本:< 0.1ms
// memo 的比较成本:~0.05ms
// 得不偿失!

6.2 判断标准

值得 memo 的场景

  • 组件渲染时间 > 1ms
  • 组件在频繁更新的父组件下
  • 组件有大量子组件

不值得 memo 的场景

  • 简单展示组件(渲染 < 0.5ms)
  • Props 每次都创建新引用
  • 组件本身就因为 Context 或 state 变化而频繁更新

6.3 更好的优化思路

优化数据流,而不是依赖 memo

// 不好的做法:依赖 memo 来阻止不必要的渲染
const Child = React.memo(function Child({ data }) {
  return <div>{data.name}</div>;
});

function Parent() {
  const [count, setCount] = useState(0);

  // data 每次都是新引用
  const data = { name: 'John' };

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <Child data={data} />
    </>
  );
}

// 好的做法:让 data 稳定
function Parent() {
  const [count, setCount] = useState(0);

  // data 保持稳定引用
  const data = useMemo(() => ({ name: 'John' }), []);

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      <Child data={data} />
    </>
  );
}

6.4 useCallback 和 useMemo 的配合

当 props 是函数时,配合 useCallback 可以让 memo 更有效:

function Parent() {
  const [count, setCount] = useState(0);

  // useCallback 确保这个函数引用稳定
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>{count}</button>
      {/* handleClick 引用稳定,MemoizedButton 不会被不必要的重新渲染 */}
      <MemoizedButton onClick={handleClick} />
    </>
  );
}

七、面试高频问题

Q: React.memo 和 useMemo 的区别是什么?

回答要点:React.memo 是 HOC,用于包装组件,使其在 props 没变化时跳过渲染,缓存的是组件的渲染结果。useMemo 是 Hook,用于缓存任意计算结果,缓存的是值本身,不是组件。选择标准:组件渲染优化用 React.memo,值计算缓存用 useMemo。两者可以配合使用——用 useMemo 稳定 props 的引用,用 React.memo 防止 props 没变化时重新渲染。

Q: 什么时候不应该用 React.memo?

回答要点:不应该用 memo 的场景包括:组件渲染本身很快(memo 比较开销可能更大)、props 每次都是新引用(memo 比较永远失败)、组件本身因为 Context 或 state 变化而频繁更新(memo 形同虚设)。过早优化是常见错误,应该先测量再优化。

Q: PureComponent 和 React.memo 有什么区别?

回答要点:两者做的事情本质上相同——浅比较 props/state,决定是否重新渲染。区别是 PureComponent 用于类组件,通过重写 shouldComponentUpdate 实现;React.memo 用于函数组件,是 HOC 形式。在现代 React 开发中,函数组件 + React.memo 是推荐做法,PureComponent 主要用于遗留类组件的优化。


延展阅读