memo 允许你在组件的 props 没有改变时跳过重新渲染。

const MemoizedComponent = memo(SomeComponent, arePropsEqual?)

参考

memo(Component, arePropsEqual?)

将组件包装在 memo 中以获得该组件的已记忆版本。只要其 props 没有更改,此组件的已记忆版本通常不会在其父组件重新渲染时重新渲染。但是 React 仍然可能会重新渲染它:记忆化是一种性能优化,而不是保证。

import { memo } from 'react';

const SomeComponent = memo(function SomeComponent(props) {
// ...
});

请参见下面的更多示例。

参数

  • Component: 你想要记忆化的组件。memo 不会修改此组件,而是返回一个新的已记忆组件。任何有效的 React 组件,包括函数和 forwardRef 组件,都是可以接受的。

  • 可选 arePropsEqual:一个接受两个参数的函数:组件之前的 props 和其新的 props。如果旧的和新的 props 相等,它应该返回 true:也就是说,如果组件使用新的 props 渲染的结果和行为与旧的 props 相同。否则,它应该返回 false。通常,你不会指定此函数。默认情况下,React 将使用 Object.is 来比较每个 prop。

返回

memo 返回一个新的 React 组件。它的行为与提供给memo 的组件相同,只是除非其 props 发生更改,否则 React 不会总是在其父组件重新渲染时重新渲染它。


用法

跳过 props 未更改时的重新渲染

React 通常在其父组件重新渲染时重新渲染组件。使用memo,您可以创建一个组件,只要其新的 props 与旧的 props 相同,React 就不会在父组件重新渲染时重新渲染它。这样的组件被称为已记忆化的。

要记忆化一个组件,请将其包裹在memo 中,并使用其返回值替换原始组件。

const Greeting = memo(function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
});

export default Greeting;

React 组件应始终具有纯渲染逻辑。这意味着如果其 props、状态和上下文没有更改,它必须返回相同的输出。通过使用memo,您告诉 React 您的组件符合此要求,因此只要其 props 没有更改,React 就无需重新渲染。即使使用memo,如果组件自己的状态发生更改或其使用的上下文发生更改,组件也会重新渲染。

在这个例子中,请注意,只要name 更改(因为它是其 props 之一),Greeting 组件就会重新渲染,但当address 更改时不会重新渲染(因为它没有作为 prop 传递给Greeting)。

import { memo, useState } from 'react';

export default function MyApp() {
  const [name, setName] = useState('');
  const [address, setAddress] = useState('');
  return (
    <>
      <label>
        Name{': '}
        <input value={name} onChange={e => setName(e.target.value)} />
      </label>
      <label>
        Address{': '}
        <input value={address} onChange={e => setAddress(e.target.value)} />
      </label>
      <Greeting name={name} />
    </>
  );
}

const Greeting = memo(function Greeting({ name }) {
  console.log("Greeting was rendered at", new Date().toLocaleTimeString());
  return <h3>Hello{name && ', '}{name}!</h3>;
});

注意

您应该只将memo 作为性能优化手段。如果您的代码没有它就不能工作,请首先找到根本问题并修复它。然后,您可以添加memo 来提高性能。

深入探讨

你应该在所有地方都添加 memo 吗?

如果您的应用程序像本网站一样,并且大多数交互都很粗略(例如替换页面或整个部分),则通常不需要记忆化。另一方面,如果您的应用程序更像是一个绘图编辑器,并且大多数交互都很细致(例如移动形状),那么您可能会发现记忆化非常有用。

只有当您的组件经常使用完全相同的 props 重新渲染,并且其重新渲染逻辑代价高昂时,使用memo 进行优化才有价值。如果您的组件重新渲染时没有明显的延迟,则memo 是不必要的。请记住,如果传递给组件的 props *始终不同*,例如传递在渲染期间定义的对象或普通函数,则memo 完全没有用。这就是为什么您通常需要将useMemouseCallbackmemo 一起使用。

在其他情况下,将组件包装在memo 中没有任何好处。这样做也没有明显的坏处,因此一些团队选择不考虑个别情况,而尽可能多地进行记忆化。这种方法的缺点是代码的可读性降低。此外,并非所有记忆化都是有效的:单个“始终为新”的值足以破坏整个组件的记忆化。

实际上,您可以通过遵循以下几个原则来避免许多不必要的记忆化。

  1. 当组件在视觉上包装其他组件时,让它接受 JSX 作为子元素。这样,当包装器组件更新其自身状态时,React 就会知道其子元素不需要重新渲染。
  2. 优先使用局部状态,并且不要提升状态到比必要更高级别。例如,不要将表单等瞬态状态以及项目是否悬停保留在树的顶部或全局状态库中。
  3. 保持您的渲染逻辑纯净。如果重新渲染组件导致问题或产生一些明显的视觉伪影,那就是组件中的错误!修复错误而不是添加记忆化。
  4. 避免不必要的更新状态的 Effects。React 应用程序中的大多数性能问题都是由源自 Effects 的更新链引起的,这些更新链会导致您的组件反复渲染。
  5. 尝试从您的 Effects 中删除不必要的依赖项。例如,与其进行记忆化,不如将某些对象或函数移到 Effect 内部或组件外部通常更简单。

如果特定交互仍然感觉滞后,请使用 React 开发者工具分析器查看哪些组件最能从记忆化中受益,并在需要的地方添加记忆化。这些原则使您的组件更容易调试和理解,因此无论如何都最好遵循它们。从长远来看,我们正在研究自动进行细粒度记忆化以一劳永逸地解决这个问题。


使用状态更新已记忆化的组件

即使组件已记忆化,当其自身状态发生更改时,它仍然会重新渲染。记忆化只与从其父组件传递给组件的 props 有关。

import { memo, useState } from 'react';

export default function MyApp() {
  const [name, setName] = useState('');
  const [address, setAddress] = useState('');
  return (
    <>
      <label>
        Name{': '}
        <input value={name} onChange={e => setName(e.target.value)} />
      </label>
      <label>
        Address{': '}
        <input value={address} onChange={e => setAddress(e.target.value)} />
      </label>
      <Greeting name={name} />
    </>
  );
}

const Greeting = memo(function Greeting({ name }) {
  console.log('Greeting was rendered at', new Date().toLocaleTimeString());
  const [greeting, setGreeting] = useState('Hello');
  return (
    <>
      <h3>{greeting}{name && ', '}{name}!</h3>
      <GreetingSelector value={greeting} onChange={setGreeting} />
    </>
  );
});

function GreetingSelector({ value, onChange }) {
  return (
    <>
      <label>
        <input
          type="radio"
          checked={value === 'Hello'}
          onChange={e => onChange('Hello')}
        />
        Regular greeting
      </label>
      <label>
        <input
          type="radio"
          checked={value === 'Hello and welcome'}
          onChange={e => onChange('Hello and welcome')}
        />
        Enthusiastic greeting
      </label>
    </>
  );
}

如果将状态变量设置为其当前值,即使没有memo,React 也会跳过重新渲染组件。您仍然可能会看到组件函数被额外调用一次,但结果将被丢弃。


使用上下文更新记忆组件

即使组件被记忆化,当它使用的上下文发生变化时,它仍然会重新渲染。记忆化只与从父组件传递给组件的props有关。

import { createContext, memo, useContext, useState } from 'react';

const ThemeContext = createContext(null);

export default function MyApp() {
  const [theme, setTheme] = useState('dark');

  function handleClick() {
    setTheme(theme === 'dark' ? 'light' : 'dark'); 
  }

  return (
    <ThemeContext.Provider value={theme}>
      <button onClick={handleClick}>
        Switch theme
      </button>
      <Greeting name="Taylor" />
    </ThemeContext.Provider>
  );
}

const Greeting = memo(function Greeting({ name }) {
  console.log("Greeting was rendered at", new Date().toLocaleTimeString());
  const theme = useContext(ThemeContext);
  return (
    <h3 className={theme}>Hello, {name}!</h3>
  );
});

为了使您的组件仅在上下文的一部分发生变化时重新渲染,请将您的组件拆分成两个。在外部组件中读取您需要的内容,并将其作为prop传递给记忆化的子组件。


最小化props变化

当您使用 memo 时,只要任何 prop 与之前的 prop **浅比较** 不相等,您的组件就会重新渲染。这意味着 React 使用 Object.is 比较来比较组件中的每个 prop 与其之前的 value。请注意,Object.is(3, 3)true,但 Object.is({}, {})false

为了充分利用 memo,请最大限度地减少 props 变化的次数。例如,如果 prop 是一个对象,请使用 useMemo 来防止父组件每次都重新创建该对象:

function Page() {
const [name, setName] = useState('Taylor');
const [age, setAge] = useState(42);

const person = useMemo(
() => ({ name, age }),
[name, age]
);

return <Profile person={person} />;
}

const Profile = memo(function Profile({ person }) {
// ...
});

最小化 props 变化的更好方法是确保组件在其 props 中接受最少必要的信息。例如,它可以接受单个值而不是整个对象。

function Page() {
const [name, setName] = useState('Taylor');
const [age, setAge] = useState(42);
return <Profile name={name} age={age} />;
}

const Profile = memo(function Profile({ name, age }) {
// ...
});

即使是单个值有时也可以投影到变化频率较低的值。例如,这里一个组件接受一个布尔值来指示值的存在,而不是值本身。

function GroupsLanding({ person }) {
const hasGroups = person.groups !== null;
return <CallToAction hasGroups={hasGroups} />;
}

const CallToAction = memo(function CallToAction({ hasGroups }) {
// ...
});

当您需要将函数传递给记忆化组件时,要么在组件外部声明它,使其永不改变,要么使用 useCallback 来缓存其定义,使其在重新渲染之间不会变化。


指定自定义比较函数

在极少数情况下,最小化记忆组件的 props 变化可能是不可行的。在这种情况下,您可以提供一个自定义比较函数,React 将使用该函数来比较旧的和新的 props,而不是使用浅比较。此函数作为第二个参数传递给 memo。只有当新的 props 导致与旧的 props 相同的输出时,它才应该返回 true;否则,它应该返回 false

const Chart = memo(function Chart({ dataPoints }) {
// ...
}, arePropsEqual);

function arePropsEqual(oldProps, newProps) {
return (
oldProps.dataPoints.length === newProps.dataPoints.length &&
oldProps.dataPoints.every((oldPoint, index) => {
const newPoint = newProps.dataPoints[index];
return oldPoint.x === newPoint.x && oldPoint.y === newPoint.y;
})
);
}

如果您这样做,请使用浏览器开发者工具中的性能面板来确保您的比较函数实际上比重新渲染组件更快。您可能会感到惊讶。

进行性能测量时,请确保 React 处于生产模式。


故障排除

我的组件在 prop 为对象、数组或函数时重新渲染

React 通过浅比较来比较旧的和新的 props:也就是说,它考虑每个新的 prop 是否与旧的 prop 引用相等。如果您每次父组件重新渲染时都创建一个新的对象或数组,即使各个元素都相同,React 仍然会认为它已更改。类似地,如果您在渲染父组件时创建了一个新函数,即使该函数具有相同的定义,React 也会认为它已更改。为避免这种情况,简化 props 或在父组件中记忆化 props