Kodokon kodokon.com

Мемоизация: memo, useMemo, useCallback

Разберись, когда React.memo, useMemo и useCallback действительно ускоряют приложение, а когда только зря его усложняют.

10 мин · 3 вопросов

Открыть этот урок в Kodokon

Когда компонент перерисовывается, по умолчанию перерисовываются все его дети, независимо от того, менялись их пропсы или нет. Это не трагедия: рендер трогает DOM, только если изменился результат. Мемоизация становится уместной там, где частые рендеры встречаются с дорогими поддеревьями. Три инструмента, три цели: React.memo пропускает рендер компонента, если его пропсы поверхностно равны, useMemo кэширует вычисленное значение, useCallback кэширует функцию. Все трое опираются на равенство ссылок.

JSX
import { memo } from "react";

const Row = memo(function Row({ item, onSelect }) {
  return (
    <li onClick={() => onSelect(item.id)}>{item.label}</li>
  );
});
Row перерисовывается, только если у item или onSelect изменилась ссылка.

Цепочка прочна ровно настолько, насколько прочно её слабое звено. Если хотя бы один пропс получает новую ссылку при каждом рендере - встроенная стрелочная функция, литерал объекта или массива - сравнение в memo проваливается, и компонент всё равно перерисовывается. useCallback стабилизирует ссылку на функцию, чтобы сравнение сработало.

JSX
import { useCallback } from "react";

function List({ items }) {
  const handleSelect = useCallback((id) => {
    console.log("selected:", id);
  }, []);

  return (
    <ul>
      {items.map((item) => (
        <Row
          key={item.id}
          item={item}
          onSelect={handleSelect}
        />
      ))}
    </ul>
  );
}
handleSelect сохраняет одну и ту же ссылку при каждом рендере.

У useMemo два законных применения: избежать по-настоящему дорогого вычисления (сортировка или агрегация тысяч элементов) и стабилизировать ссылку, которую потребляет мемоизированный ребёнок или которая указана в зависимостях эффекта. Для тривиального вычисления кэш стоит дороже, чем экономит: сравнение зависимостей, удерживаемая память, более тяжёлое чтение кода.

JSX
import { useMemo } from "react";

function Stats({ orders }) {
  const total = useMemo(() => {
    return orders.reduce(
      (sum, order) => sum + order.amount,
      0
    );
  }, [orders]);

  return <p>Total: {total} €</p>;
}
Итог пересчитывается, только если у orders изменилась ссылка.

Проверка знаний

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

  1. Ребёнок обёрнут в React.memo, но родитель передаёт ему встроенную стрелочную функцию как пропс. Результат?
    • Мемоизация работает: функции сравниваются по их исходному коду
    • Мемоизация бесполезна: встроенная функция создаёт новую ссылку при каждом рендере, и сравнение проваливается
    • React выдаёт предупреждение в режиме разработки
  2. Что гарантирует useCallback(fn, [a]), пока a не меняется?
    • Что функция никогда не создаётся в памяти заново
    • Что функция выполнится только один раз
    • Что от рендера к рендеру возвращается одна и та же ссылка на функцию
  3. В каком случае useMemo однозначно оправдан?
    • Дорогое вычисление или ссылка, которую нужно стабилизировать для мемоизированного ребёнка либо для зависимости эффекта
    • Любое производное значение, на всякий случай
    • Кэширование данных между двумя монтированиями компонента