React.memo, useMemo, useCallback: Когда они помогают, а когда вредят

#react#performance#memoization

TL;DR: Использование React.memo, useMemo и useCallback должно быть обоснованным. Преждевременная оптимизация может добавить накладные расходы без реальной пользы. Профилируйте перед мемоизацией и применяйте эти инструменты только там, где это действительно необходимо.

Введение: Контекст и актуальность

В мире React разработки оптимизация производительности — это важная тема. Однако часто разработчики злоупотребляют инструментами мемоизации, такими как React.memo, useMemo и useCallback, добавляя их везде, где только можно. Это может привести к обратному эффекту: вместо ускорения приложения вы получаете дополнительные накладные расходы на сравнение пропсов и использование памяти.

В этой статье мы разберем, когда использование этих инструментов действительно помогает, а когда оно только вредит производительности и читаемости кода.

Основная часть с примерами кода

React.memo: Когда это работает, а когда нет

React.memo — это Higher-Order Component (HOC), который оборачивает функциональный компонент и выполняет поверхностное сравнение пропсов перед принятием решения о повторном рендеринге. Если родительский компонент перерендеривается, но передает те же пропсы (по ссылке для объектов, по значению для примитивов), React пропускает рендеринг дочернего компонента.

const Button = React.memo(function Button({ onClick, label }) {
  console.log("Button rendered");
  return <button onClick={onClick}>{label}</button>;
});

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

  // Это ломает мемоизацию — новый объект каждый рендер
  const style = { color: "red" };

  // Это нормально — примитив строка, та же ссылка
  return <Button onClick={() => setCount(c => c + 1)} label="Click me" style={style} />;
}

В приведенном выше примере Button будет перерендериваться при каждом рендере Parent, несмотря на React.memo, потому что style и onClick создаются заново каждый раз.

useMemo: Для дорогих вычислений

useMemo кэширует результат вычислений между рендерами. Он пересчитывает значение только при изменении зависимостей.

const sortedItems = useMemo(() => {
  return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);

Важно понимать, что useMemo не делает вычисления бесплатными. Они все равно выполняются на первом рендере, и на каждом последующем рендере запускается сравнение зависимостей. Если вычисления дешевые (например, несколько арифметических операций), useMemo может стоить больше, чем он экономит.

useCallback: Стабильные ссылки на функции

useCallback по сути является useMemo для функций. Он кэширует ссылку на функцию между рендерами, чтобы идентификатор функции оставался стабильным, пока не изменятся зависимости.

const handleSelect = useCallback((id) => {
  setSelectedId(id);
  onSelectionChange(id);
}, [onSelectionChange]);

useCallback полезен, когда вы передаете стабильные ссылки на функции в мемоизированные дочерние компоненты. Без него даже если дочерний компонент обернут в React.memo, любая функция, переданная из родительского компонента, будет новой ссылкой на каждый рендер, что нарушит мемоизацию.

Практическое применение

Когда мемоизация помогает:

Когда мемоизация вредит:

Заключение

Мемоизация в React — это мощный инструмент, но его нужно использовать с умом. Прежде чем добавлять React.memo, useMemo или useCallback, всегда профилируйте ваше приложение, чтобы понять, где действительно есть проблемы с производительностью. Неоправданное использование этих инструментов может привести к увеличению накладных расходов и снижению читаемости кода. Помните: профилируйте сначала, мемоизируйте потом.


Источник: https://dev.to/helloashish99/reactmemo-usememo-usecallback-when-they-help-vs-hurt-4f6j