React.memo: мемоизация компонента

Урок про React.memo: как пропустить рендер компонента, когда его пропсы не изменились, и где это даёт эффект, а где — нет.

React.memo — обёртка над компонентом, которая мемоизирует результат: если пропсы поверхностно равны прошлым, React переиспользует прошлый рендер вместо нового вызова функции.

Зачем

Из первого урока мы знаем: рендер родителя по умолчанию рендерит всех детей. Если ребёнок дорогой, а его пропсы при этом не менялись, его рендер — лишняя работа. React.memo ставит «сторожа»: перед рендером он сравнивает новые пропсы со старыми. Если все пропсы равны (по Object.is, поверхностно), компонент не рендерится.

const ExpensiveChild = React.memo(function ExpensiveChild({ label }) {
  console.log("ExpensiveChild рендерится");
  return <p>{label}</p>;
});

function Parent() {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      <ExpensiveChild label="Я не завишу от count" />
    </div>
  );
}

Теперь клики по кнопке не печатают «ExpensiveChild рендерится»: проп label — одна и та же строка, поверхностное сравнение проходит, рендер пропускается.

Подвох: новые ссылки ломают memo

Сравнение поверхностное и работает по ссылке для объектов, массивов и функций. Если родитель создаёт новый объект или функцию на каждый рендер и передаёт их пропсом, memo бесполезен — ссылки всегда разные.

// memo НЕ сработает: новый объект style и новая функция onClick каждый рендер
<ExpensiveChild style={{ color: "red" }} onClick={() => doThing()} />

Поэтому React.memo почти всегда идёт в паре с useMemo/useCallback, которые стабилизируют ссылки на объекты и функции — это тема следующего урока.

Кастомное сравнение

Вторым аргументом React.memo принимает функцию areEqual(prevProps, nextProps). Верните true, если рендер можно пропустить. Нужна редко — обычно как «затычка», когда проп — глубокий объект, который вы готовы сравнивать вручную. Чаще правильнее передавать примитивы или стабильные ссылки.

Когда memo вреден

  • Компонент лёгкий: сравнение пропсов стоит дороже, чем сам рендер.
  • Пропсы и так меняются почти каждый раз: memo только добавляет проверку, рендер всё равно происходит.
  • Пропсы — нестабильные ссылки (объекты/функции без мемоизации): memo не сработает, а вы думаете, что оптимизировали.

Правило: оборачивайте в React.memo компонент, который (а) реально дорогой и (б) часто получает неизменные пропсы при рендерах родителя. Проверьте эффект в Profiler.

Итог

  • React.memo пропускает рендер, если пропсы поверхностно равны прошлым.
  • Сравнение по ссылке: новые объекты/функции в пропсах сводят memo на нет.
  • Сочетайте с useMemo/useCallback для стабильных ссылок.
  • Не оборачивайте дешёвые компоненты — это пессимизация.
Проверьте себя
1. Что делает React.memo?
AКэширует результат функции внутри компонента
BПропускает рендер компонента, если пропсы поверхностно равны прошлым
CЗапрещает компоненту иметь состояние
DУскоряет все вычисления автоматически
2. Почему передача `style={{ color: 'red' }}` ломает React.memo?
AInline-стили запрещены в React
BОбъект-литерал создаётся заново каждый рендер, ссылка всегда новая
Cmemo не сравнивает строки
Dcolor нельзя передавать пропсом
3. Когда оборачивать компонент в React.memo НЕ стоит?
AКогда он дорогой и пропсы стабильны
BКогда он лёгкий или пропсы и так меняются каждый раз
CКогда он находится в списке
DКогда у него есть useState
Поддержать проект