Menu

React.memo: пропуск повторных рендеров при тех же пропсах

memo оборачивает компонент так, что React пропускает его повторный рендер, когда пропсы поверхностно равны прошлым. Когда он помогает, почему новые объекты и функции в пропсах сводят его на нет и чего он не останавливает.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

memo позволяет компоненту пропускать повторный рендер, когда его пропсы такие же, как в прошлый раз. Оберните компонент в memo(...), и когда его родитель рендерится, React сначала сравнивает каждый новый пропс со старым; если все равны, он переиспользует предыдущий результат вместо того, чтобы снова вызывать компонент.

Пишите в поле: оно успевает за вводом, а Console показывает render SlowList только один раз, от первого рендера. Уберите обёртку memo( (и её закрывающую скобку) и снова попечатайте: теперь каждое нажатие клавиши ждёт список 150 мс и выводит рендер.

Синтаксис

import { memo } from 'react';

const MemoizedComponent = memo(SomeComponent, arePropsEqual?);
  • SomeComponent это любой функциональный компонент. Обычный стиль это оборачивать определение напрямую: const Card = memo(function Card(props) { ... }). Имя у внутренней функции сохраняет имя в React DevTools и в сообщениях об ошибках.
  • arePropsEqual необязателен и нужен редко (см. ниже).
  • Результат это компонент, который рендерится как любой другой.

По умолчанию React сравнивает старые и новые пропсы поверхностно: проверяет каждый пропс через Object.is. Строки, числа и булевы значения сравниваются по значению, поэтому title="Hi" равен title="Hi". Объекты, массивы и функции сравниваются по ссылке: два объекта с одинаковым содержимым всё равно разные объекты.

В примере выше items объявлен вне компонента, поэтому при каждом рендере это один и тот же массив, и сравнение проходит.

Новые объекты и функции сводят memo на нет

Всё, что написано литералом в теле родителя, создаётся заново при каждом рендере: { name: 'Ada' }, [1, 2, 3], () => save(). Передайте такое компоненту с memo, и пропс никогда не будет равен предыдущему.

Каждый клик выводит render Badge object prop, но никогда строковую версию. Случай с объектом можно исправить тремя способами:

  • Передавайте примитивы. name="Ada" вместо user={{ name: 'Ada' }}, когда дочернему компоненту нужно одно-два поля.
  • Выносите константы из компонента. Объект, который никогда не меняется, может жить на уровне модуля, как items в первом примере.
  • Кэшируйте. Оборачивайте объекты и массивы, собранные из пропсов или состояния, в useMemo, а функции в useCallback.

У children та же проблема. <Card><p>Hello</p></Card> при каждом рендере создаёт новый объект элемента <p>, поэтому Card с memo, получающий children, перерендеривается каждый раз.

Чего memo не останавливает

memo смотрит только на пропсы, приходящие от родителя. Мемоизированный компонент всё равно рендерится, когда:

  • меняется его собственное состояние через его useState или useReducer;
  • меняется контекст, который он читает, через useContext.

Нажмите Unrelated, и Panel пропускается. Переключите тему, и он рендерится, потому что читает контекст. Нажмите Like, и он рендерится, потому что изменилось его собственное состояние. Так и задумано: memo нужен, чтобы пропускать работу, когда не изменилось ничего, от чего зависит компонент, а контекст и состояние это то, от чего он зависит.

Собственная функция сравнения

Второй аргумент заменяет поверхностное сравнение. Он получает предыдущие и следующие пропсы и возвращает true, когда их следует считать равными, то есть когда React пропускает рендер.

const MemoChart = memo(Chart, (prev, next) => {
    return (
        prev.points.length === next.points.length &&
        prev.points.every((p, i) => p === next.points[i]) &&
        prev.color === next.color &&
        prev.onSelect === next.onSelect
    );
});

Это нужно редко, и тут легко ошибиться. Если функция игнорирует пропс, компонент продолжает показывать его старое значение. Игнорировать пропс-функцию хуже всего: пропущенный компонент продолжает вызывать старую функцию, которая читает старое состояние. Сравнивайте каждый пропс и не делайте глубоких проверок равенства на больших данных, потому что тогда сравнение стоит дороже рендера, который оно экономит. Стабильные пропсы через useMemo и useCallback почти всегда лучшее решение.

Как проверить, что memo работает

Самая быстрая проверка та, что используется на этой странице: console.log в начале компонента. Если он выводится там, где вы ожидали пропуск, выведите и пропсы и сравните их с предыдущими, чтобы найти изменившийся. В настоящем проекте поможет React DevTools: включите в его настройках «Highlight updates when components render», чтобы видеть, какие компоненты мигают при каждом взаимодействии, а вкладка Profiler записывает каждый рендер и, с соответствующей настройкой, причину рендера каждого компонента.

Две вещи выглядят как сбой memo, но им не являются:

  • Первый рендер происходит всегда. memo пропускает рендеры только после того, как компонент один раз отрендерился с этими пропсами.
  • Сборки для разработки могут рендерить больше. Под StrictMode React в разработке рендерит каждый компонент дважды, чтобы поймать нечистый код. Превью здесь работает как продакшен-сборка, поэтому каждая строка лога выше означает один настоящий рендер.

Когда использовать memo

memo того стоит, когда компонент медленно рендерится, а его родитель часто рендерится с теми же пропсами для него: длинный список или график рядом с полем ввода, боковая панель внутри страницы, которая обновляется каждую секунду. Для компонентов, которые рендерятся быстро или чьи пропсы всё равно меняются каждый раз, он добавляет сравнение и ничего не даёт. Прежде чем мемоизировать, посмотрите, нельзя ли вместо этого перестроить код, перенеся состояние ниже или передавая JSX как children, как показано на странице о рендеринге.

React Compiler 1.0, если ваш проект его использует, автоматически мемоизирует компоненты и их пропсы во время сборки, и большинство ручных вызовов memo становятся не нужны. Без него memo работает ровно так, как описано здесь, и остаётся распространённым в коде на React.

Часто задаваемые вопросы

Что делает React.memo?

Он возвращает мемоизированную версию компонента. Когда родитель рендерится, React сравнивает новые пропсы с предыдущими и пропускает рендер компонента, если каждый пропс равен по Object.is.

Почему мой компонент с memo всё равно перерендеривается?

Обычно потому, что пропс это новый объект, массив или функция, созданные во время рендера родителя, поэтому он никогда не равен предыдущему. Компонент также рендерится, когда меняется его собственное состояние или контекст, который он читает, с memo или без.

Чем React.memo отличается от useMemo?

memo оборачивает компонент и пропускает его рендер. useMemo это хук внутри компонента, который кэширует вычисленное значение. Они часто работают вместе: useMemo сохраняет пропс-объект стабильным, чтобы memo мог пропустить рендер.

Оборачивать ли каждый компонент в memo?

Нет. Большинство рендеров дешёвые, а у сравнения пропсов своя цена. Используйте его для компонентов, которые медленно рендерятся и часто получают те же пропсы.

Работает ли memo с children?

Редко. JSX, переданный как children, при каждом рендере новый объект, поэтому компонент с memo, получающий children, почти всегда перерендеривается.

Иллюстрация языков программирования Coddy

Учитесь программировать с Coddy

НАЧАТЬ