4. React.memo:纯组件与浅比较

React 默认的组件重渲染机制是不加判断的——只要父组件状态变化,整个子树都会重新执行 render 函数,即使子组件的 props 没有发生任何改变。当子树中节点数量多或计算量大的时候,这种“无差别”重渲染会造成可感知的性能浪费。React.memo 是为函数组件提供的性能优化方案,它通过浅比较新旧 props 来决定是否跳过本次渲染,让组件表现出“纯组件”的行为。

工作原理

React.memo 是一个高阶组件,接收一个函数组件和一个可选的比较函数,返回被缓存的新组件。每当父组件渲染时,React 会先调用比较函数(默认为浅比较),判断新 props 与上一次渲染时的 props 是否相等。若相等,则直接复用上次的虚拟 DOM 结果,跳过本次 render 执行;若不相等,则正常重新渲染。

关键优化点:避免因父组件无关状态变化导致子组件不必要的 render 执行。memo 只针对 props 变化做拦截,不干扰内部 state 或 context 变化。

基本使用

使用 React.memo 包裹函数组件即可,通常不需要手动传递比较函数。

import React, { useState } from 'react';

// 未优化的子组件
const Child = ({ name }) => {
  console.log('Child rendered');
  return <div>{name}</div>;
};

// 使用 React.memo 包裹
const MemoChild = React.memo(Child);

const Parent = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Alice');
  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>增加计数</button>
      <button onClick={() => setName('Bob')}>修改名称</button>
      <MemoChild name={name} />
    </div>
  );
};

上述示例中,点击“增加计数”时,count 变化触发父组件重新渲染,但 name 未变,MemoChild 的浅比较返回 true,控制台不会打印 “Child rendered”,从而避免无效渲染。

浅比较的限制

React.memo 默认使用 Object.is 对 props 对象的每个属性进行浅比较。当 props 中包含对象、数组、函数引用时,浅比较会失效,因为每次渲染都会生成新的对象引用。

典型陷阱:如果给 memo 组件传递内联对象或箭头函数,每次父组件渲染都会创建新引用,导致浅比较永远返回 false,memo 完全失效。
// 以下写法会让 memo 失效
<MemoChild name={{ firstName: 'Alice' }} />
<MemoChild onClick={() => handleClick()} />

解决方案:使用 useMemo 稳定对象引用,使用 useCallback 稳定函数引用。

自定义比较函数

当浅比较无法满足需求时,可以通过 React.memo 的第二个参数传递自定义比较函数。该函数接收 (prevProps, nextProps),返回 true 表示 props 相等(跳过渲染),返回 false 表示不等(重新渲染)。

示例:深比较

import React from 'react';
import isEqual from 'lodash.isequal';

const DeepMemoChild = React.memo(
  ({ data }) => {
    return <div>{JSON.stringify(data)}</div>;
  },
  (prevProps, nextProps) => {
    // 使用 lodash 深比较
    return isEqual(prevProps.data, nextProps.data);
  }
);
性能警告:深比较代价较高,每次渲染都会执行完整递归对比。仅在确实需要比较深层嵌套数据且数据量不大时使用。对高频变化的场景,更推荐设计扁平的数据结构并配合 useMemo 生成稳定引用。

自定义比较函数注意事项

  • 必须严格按照 (prevProps, nextProps) => boolean 签名。
  • 如果比较函数返回 true,React 会完全跳过 render 以及后续的 diff 过程。
  • 不要尝试在比较函数中执行副作用或异步操作。
  • 比较函数本身不应产生额外渲染开销,建议保持轻量。

React.memo 工作流程

React.memo 决策流程 父组件渲染 生成新 props 对象 进入 React.memo props 相等? 跳过渲染 正常重渲染 注:默认浅比较使用 Object.is;自定义比较函数可覆盖此行为。

调试技巧

  • 在 React DevTools 中,被 memo 包裹的组件会显示 “Memo” 标记,点击后可以查看 “Renders” 信息。
  • 启用 DevTools 的 “Highlight updates when components render” 可以直观看到哪些组件没有被 memo 阻止。
  • 在比较函数中加入 console.log 可辅助判断比较结果。

常见错误与避坑

错误模式表现修复
传内联对象 / 函数memo 失效,子组件总重渲染使用 useMemo / useCallback 稳定引用
忘记包裹子组件性能问题未解决对 每个 可能因父组件无关状态重渲染的组件添加 memo
自定义比较函数返回 false 导致从不更新组件卡在旧状态确保比较逻辑正确,考虑非 props 变化(如 context)的处理
在 memo 组件中使用内联样式对象每次渲染生成新对象,memo 失效将样式对象提取到组件外部或用 useMemo 缓存

性能优化建议

  • 配合 useCallback / useMemo:传递给 memo 组件的回调函数和数据对象必须稳定引用,否则 memo 形同虚设。
  • 避免滥用:memo 本身有比较开销,仅对渲染成本高且 props 变化频率低的组件使用。
  • 优先扁平化 props:将复杂对象拆解为基本类型属性,让浅比较能正常工作。
  • 测量后再优化:使用 React Profiler 找出实际造成性能瓶颈的组件,针对性地添加 memo。

对比:React.memo vs PureComponent vs shouldComponentUpdate

特性React.memoPureComponentshouldComponentUpdate
适用组件类型函数组件类组件类组件
默认比较方式浅比较 props浅比较 props + state无(需手动实现)
自定义比较第二个参数:areEqual不可直接自定义(需重写 lifecycle)返回 true / false
性能开销每次渲染执行比较函数与 memo 类似开发者控制