创见博客
React.memo 为什么没生效?讲清原理、用法与失效场景
七崽爱吃小饼干2026/10/21阅读 0专栏 React

一个常见的场景:

父组件里有一个输入框,用户每输入一个字符,父组件都会重新渲染。父组件下面还有一个渲染开销很大的图表组件,但它的 props 根本没有变化,却也跟着重新渲染。

这时就会想到 React.memo。

但 React.memo 不是“给所有组件包一层就变快”的万能药。它解决什么、不解决什么,需要先分清。

一、React.memo 是什么

React.memo 是 React 提供的一个高阶组件,用来缓存函数组件的渲染结果。

当父组件重新渲染时,React 会先比较组件的 props:

  • props 浅比较相等:跳过这次渲染,复用上一次的结果。
  • props 不相等:正常重新渲染。

基本用法:

jsx
import { memo } from 'react';

const Child = memo(function Child({ title }) {
  console.log('Child render');

  return <h1>{title}</h1>;
});

之后父组件重新渲染,只要 title 没变,Child 就不会重新执行。

注意,React.memo 比较的是 props,不是组件的内部状态,也不是 context。

二、为什么需要 React.memo

React 的默认行为是:父组件重新渲染时,子组件也会重新渲染。

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

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

      <ExpensiveChild title="固定标题" />
    </div>
  );
}

点击按钮时,Parent 重新渲染,ExpensiveChild 也会重新执行。即使 title 一直是 "固定标题"。

如果 ExpensiveChild 内部有复杂的计算、较大的列表或图表,这就是不必要的开销。

用 memo 包一层:

jsx
const ExpensiveChild = memo(function ExpensiveChild({ title }) {
  return <h1>{title}</h1>;
});

父组件再次渲染时,React 会浅比较 ExpensiveChild 的 props:

text
上一次 props: { title: '固定标题' }
这一次 props: { title: '固定标题' }

比较结果相等,于是跳过渲染。

这就是 React.memo 的主要价值:避免父组件更新导致 props 未变的子组件重复渲染。

三、怎么用

1. 基本用法

jsx
import { memo } from 'react';

const UserCard = memo(function UserCard({ name, age }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>{age}</p>
    </div>
  );
});

可以把 memo 理解为给组件加了一层“props 缓存”。

2. props 必须是稳定引用

React.memo 默认做浅比较。

浅比较的规则是:

  • 原始类型按值比较:1 === 1、'a' === 'a'。
  • 对象、数组、函数按引用比较。

所以下面这种写法,memo 基本不会生效:

jsx
function Parent() {
  return (
    <Child
      user={{ name: 'Tom' }}
      onClick={() => console.log('click')}
    />
  );
}

父组件每次渲染,user 和 onClick 都是新引用,浅比较一定不相等。

需要把引用稳定下来:

jsx
import { useCallback, useMemo } from 'react';

function Parent() {
  const user = useMemo(() => ({ name: 'Tom' }), []);

  const handleClick = useCallback(() => {
    console.log('click');
  }, []);

  return <Child user={user} onClick={handleClick} />;
}

这样 memo 才有机会命中。

3. 自定义比较函数

memo 的第二个参数可以自定义比较逻辑:

jsx
const UserCard = memo(
  function UserCard({ user }) {
    return <div>{user.name}</div>;
  },
  (prevProps, nextProps) => {
    return prevProps.user.id === nextProps.user.id;
  }
);

比较函数返回 true 表示“props 相等,可以跳过渲染”;返回 false 表示“需要重新渲染”。

自定义比较要谨慎。如果漏掉了真正影响渲染的字段,组件就会显示旧数据,而且这种问题很难排查。大多数情况下,默认的浅比较已经够用。

4. 和 useMemo、useCallback 的区别

这三个 API 经常一起出现,但作用不同:

API缓存对象解决的问题
React.memo组件渲染结果props 不变时跳过子组件渲染
useMemo一个计算结果避免重复执行昂贵计算
useCallback一个函数引用避免函数每次渲染都创建新引用

简单记:

  • memo 缓存组件。
  • useMemo 缓存值。
  • useCallback 缓存函数。

它们不是互相替代的关系。memo 负责“要不要渲染”,useMemo 和 useCallback 负责“传给它的 props 稳不稳定”。

四、React.memo 解决不了什么

1. 解决不了 context 更新

jsx
const Child = memo(function Child() {
  const { theme } = useContext(ThemeContext);

  return <div>{theme}</div>;
});

即使 Child 没有 props,只要 ThemeContext 的 value 变了,Child 仍然会重新渲染。

因为 useContext 订阅的是整个 context。memo 不会比较 context 的字段,也不会阻止 context 更新。

如果只想让组件响应 context 中的某个字段,可以考虑:

  • 拆分 context。
  • 使用支持 selector 的外部状态库。
  • 在消费 context 的边界组件里取出需要的值,再传给 memo 子组件。

例如:

jsx
function ThemeConsumer() {
  const { theme } = useContext(AppContext);

  return <MemoizedPanel theme={theme} />;
}

ThemeConsumer 仍会因 context 变化而渲染,但 MemoizedPanel 可以在 theme 没变时跳过。

2. 解决不了组件自身的 state 更新

jsx
const Child = memo(function Child() {
  const [count, setCount] = useState(0);

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

点击按钮时,Child 自己的 state 变了,它当然会重新渲染。memo 只影响父组件传入 props 引起的更新。

3. 解决不了不稳定的 props

前面已经提到,对象、数组、函数每次创建新引用,浅比较就会失败。

4. 对 children 要小心

jsx
<MemoizedLayout>
  <ExpensiveChild />
</MemoizedLayout>

父组件每次渲染时,children 都是一个新创建的 React 元素对象,memo 的浅比较可能无法命中。

如果确实需要优化,可以把 children 的创建逻辑上移,或者重新设计组件边界。

五、什么时候不该用 React.memo

不是所有组件都值得包 memo。

1. 组件本身很轻

如果组件只是渲染几个文本节点,重新执行的成本可能比 props 浅比较还低。包 memo 反而增加维护成本。

2. props 经常变化

如果 props 每次渲染都不同,memo 每次都会比较失败,既没有跳过渲染,还多了一次比较。

3. 组件依赖 context

context 更新时,memo 拦不住。此时应该优先优化 context 的结构。

4. 只是为了“看起来优化”

不要凭感觉给所有组件包 memo。先用 React DevTools 的 Profiler 找到真正耗时的渲染,再决定是否优化。

六、实际使用的步骤

可以按这个顺序处理:

  1. 用 React DevTools Profiler 录制一次交互。
  2. 找到重复渲染且耗时明显的组件。
  3. 确认它的 props 在父组件更新时是否真的没变。
  4. 用 memo 包裹组件。
  5. 用 useMemo、useCallback 稳定对象和函数 props。
  6. 再次用 Profiler 验证是否减少了渲染。

如果优化后没有明显收益,或者代码变得更复杂,就可以撤回。优化应该由数据驱动,而不是由习惯驱动。

七、React.memo 和 React Compiler

React Compiler 会在编译阶段自动做一定程度的记忆化,减少手写 memo、useMemo、useCallback 的需求。

但这不意味着理解 React.memo 没用了:

  • 你仍然需要知道组件为什么会重新渲染。
  • 仍然需要识别不稳定的 props。
  • 仍然需要理解 context 更新和组件自身更新的区别。

工具可以自动优化,但排查性能问题时,底层模型仍然是那些东西。

最后

React.memo 的核心只有一句话:props 浅比较相等时,跳过函数组件的重新渲染。

它适合解决:

  • 父组件频繁更新。
  • 子组件渲染开销较大。
  • 子组件的 props 相对稳定。

它不适合解决:

  • context 更新。
  • 组件自身 state 更新。
  • props 每次都是新引用。
  • 所有组件的无差别包裹。

记住三个关键词:

text
memo      缓存组件渲染
useMemo   缓存计算结果
useCallback 缓存函数引用

把 memo 用在真正需要的地方,再配合稳定的 props,才是有效的性能优化。

评论
0/100