Навигация

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

Мемоизация — одна из ключевых техник оптимизации производительности в React. Три механизма — useMemo, useCallback и React.memo — решают единую задачу: избежать повторных вычислений и ререндеров, когда входные данные не изменились. С появлением React Compiler (стабильный в 2025 году) ручная мемоизация в значительной степени автоматизирована, но понимание механики остаётся критически важным — для чтения чужого кода, работы с legacy-проектами и осознанного проектирования компонентов.


Содержание

  1. Что такое мемоизация и зачем она нужна
  2. useMemo — мемоизация значений
  3. useCallback — мемоизация функций
  4. React.memo — мемоизация компонентов
  5. Как useMemo, useCallback и React.memo работают вместе
  6. Vue-аналоги
  7. Типичные ошибки
  8. Когда мемоизация НЕ нужна
  9. React Compiler — автоматическая мемоизация
  10. Шпаргалка
  11. Ключевые тезисы для интервью
  12. Заключение

Что такое мемоизация и зачем она нужна

Мемоизация — это кэширование результата выполнения функции для определённого набора входных аргументов. При повторном вызове с теми же аргументами возвращается сохранённый результат без повторного вычисления.

В React каждый ререндер компонента — это вызов функции-компонента. При ререндере:

  • Пересоздаются все локальные переменные и объекты.
  • Пересоздаются все функции-обработчики.
  • Дочерние компоненты получают новые ссылки на пропсы, даже если значения не изменились.

Без мемоизации это приводит к двум проблемам:

  1. Дорогостоящие вычисления выполняются при каждом рендере, даже если данные не изменились.
  2. Дочерние компоненты перерендериваются при каждом рендере родителя, даже если их пропсы не изменились — из-за новых ссылок на объекты и функции.

Vue-аналог: во Vue реактивность построена на Proxy, и компонент перерендеривается только при изменении конкретных реактивных значений, используемых в шаблоне. Функции-обработчики не пересоздаются при каждом ререндере, потому что компилятор шаблонов Vue генерирует эффективный рендер-код. В React же компонент — это обычная JS-функция, вызываемая заново при каждом обновлении, поэтому всё внутри неё пересоздаётся.


useMemo — мемоизация значений

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

Vue-аналог: computed() — прямой эквивалент. Оба кэшируют результат и пересчитывают при изменении зависимостей. Ключевое отличие: Vue computed автоматически отслеживает зависимости через Proxy-систему, а React требует явного указания массива зависимостей.

Синтаксис

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

Пример: фильтрация списка

function ProductList({ items, filterQuery }) {
  const filteredItems = useMemo(
    () => items.filter(item =>
      item.name.toLowerCase().includes(filterQuery.toLowerCase())
    ),
    [items, filterQuery]
  );

  return (
    <ul>
      {filteredItems.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

Без useMemo фильтрация выполнялась бы при каждом ререндере — даже если items и filterQuery не изменились, а ререндер вызван, например, обновлением счётчика в другом месте компонента.

Пример: производные данные

function ShoppingCart({ items }) {
  const total = useMemo(
    () => items.reduce((sum, item) => sum + item.price * item.quantity, 0),
    [items]
  );

  const tax = useMemo(() => total * 0.2, [total]);

  return (
    <div>
      <p>Subtotal: ${total}</p>
      <p>Tax: ${tax}</p>
      <p>Total: ${total + tax}</p>
    </div>
  );
}

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

  • Дорогостоящие вычисления: фильтрация, сортировка, агрегация больших массивов.
  • Стабильная ссылка на объект/массив: когда значение передаётся как пропс в мемоизированный дочерний компонент или используется как зависимость в useEffect.
  • Производное состояние: значения, вычисляемые из других значений, чтобы избежать пересчёта при нерелевантных ререндерах.

useCallback — мемоизация функций

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

Vue-аналог: прямого аналога нет. Во Vue функции-обработчики, объявленные в <script setup>, не пересоздаются при каждом обновлении компонента — компилятор шаблонов генерирует стабильные ссылки. В React же каждая функция внутри компонента — новая при каждом вызове.

Синтаксис

const memoizedFn = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

По сути useCallback(fn, deps) эквивалентен useMemo(() => fn, deps).

Пример

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

  // Без useCallback: ссылка на increment меняется при каждом ререндере
  // (в том числе при вводе текста), что вызывает ререндер ExpensiveButton
  const increment = useCallback(
    () => setCount(c => c + 1),
    []
  );

  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <p>Count: {count}</p>
      <ExpensiveButton onClick={increment} />
    </div>
  );
}

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

  • Функция передаётся как пропс в дочерний компонент, обёрнутый в React.memo.
  • Функция используется как зависимость в useEffect или другом хуке.
  • Функция передаётся в сторонние библиотеки, которые сравнивают ссылки (например, анимационные библиотеки).

Функциональное обновление — альтернатива useCallback

Если функция использует только сеттер состояния, часто можно обойтись без useCallback, применив функциональное обновление:

// ❌ useCallback с пустыми зависимостями
const increment = useCallback(() => setCount(c => c + 1), []);

// ✅ То же самое, но setCount стабилен из коробки — useCallback не нужен
const increment = () => setCount(c => c + 1);

setCount, возвращаемый из useState, имеет стабильную ссылку между рендерами — это гарантия React. Поэтому если функция-обработчик использует только функциональную форму сеттера, мемоизировать её не нужно.


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

React.memo — это HOC (компонент высшего порядка), который оборачивает компонент и пропускает его ререндер, если пропсы не изменились (сравнение через Object.is — по ссылке).

Vue-аналог: во Vue компоненты по умолчанию перерендериваются только при изменении реактивных данных, используемых в их шаблоне. Аналог React.memo — это поведение по умолчанию, а не оптимизация. Ближайший концептуальный аналог — defineAsyncComponent с suspensible или ручная проверка в beforeUpdate, но на практике во Vue такая оптимизация просто не нужна.

Синтаксис

const MemoizedComponent = React.memo(Component);

// С кастомной функцией сравнения пропсов
const MemoizedComponent = React.memo(Component, (prevProps, nextProps) => {
  return prevProps.value === nextProps.value;
});

Пример

function ExpensiveList({ items, onItemClick }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => onItemClick(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}

const MemoizedList = React.memo(ExpensiveList);

function Parent() {
  const [items] = useState([
    { id: 1, name: "Apple" },
    { id: 2, name: "Banana" },
  ]);

  const handleItemClick = useCallback((id) => {
    console.log("Clicked:", id);
  }, []);

  // MemoizedList не перерендерится, пока items и handleItemClick
  // не изменят ссылки — а они мемоизированы
  return <MemoizedList items={items} onItemClick={handleItemClick} />;
}

Кастомная функция сравнения

По умолчанию React.memo делает поверхностное сравнение (shallow compare) всех пропсов. Для глубоких сравнений или сравнения конкретных полей передайте вторую функцию:

const MemoizedChart = React.memo(
  Chart,
  (prev, next) => prev.data.length === next.data.length
);

Кастомное сравнение стоит использовать осторожно — неправильная реализация может привести к пропуску обновлений и багам.


Как useMemo, useCallback и React.memo работают вместе

Три механизма образуют единую систему оптимизации. Рассмотрим на примере:

function Dashboard({ userId }) {
  // 1. useMemo: мемоизируем объект, чтобы не пересоздавать при каждом рендере
  const filters = useMemo(
    () => ({ status: "active", userId }),
    [userId]
  );

  // 2. useCallback: мемоизируем функцию-обработчик
  const onSelect = useCallback(
    (itemId) => console.log(`Selected ${itemId} for user ${userId}`),
    [userId]
  );

  // 3. React.memo: MemoizedTable не перерендерится,
  // пока filters и onSelect не изменят ссылки
  return <MemoizedTable filters={filters} onSelect={onSelect} />;
}

const MemoizedTable = React.memo(function Table({ filters, onSelect }) {
  // Рендерится только при изменении filters или onSelect
  return <table>...</table>;
});

Что произойдёт без мемоизации:

Без мемоизацииС мемоизацией
filters — новый объект при каждом ререндереfilters — та же ссылка, пока не изменится userId
onSelect — новая функция при каждом ререндереonSelect — та же ссылка, пока не изменится userId
MemoizedTable — перерендерится каждый раз (новые пропсы)MemoizedTable — пропустит ререндер (пропсы не изменились)

Цепочка мемоизации

useMemo → стабильный объект/значение
                ↓
useCallback → стабильная функция
                ↓
React.memo → пропуск ререндера дочернего компонента

Если хотя бы одно звено разорвано — вся цепочка перестаёт работать. Например, если передать inline-объект { style: { color: "red" } } в React.memo-компонент, он будет перерендериваться каждый раз, потому что объект стиля — новая ссылка.


Vue-аналоги

ReactVueОписание
useMemo(() => val, [dep])computed(() => val)Кэширование вычислений
useCallback(() => fn, [dep])Нет аналогаФункции во Vue стабильны по умолчанию
React.memo(Component)Поведение по умолчаниюКомпоненты во Vue перерендериваются только при изменении используемых реактивных данных

Почему во Vue не нужна ручная мемоизация

Vue использует систему реактивности на Proxy. Каждый компонент — это эффект, который отслеживает конкретные реактивные значения, использованные в шаблоне. При изменении значения перерендериваются только те компоненты, которые его читают.

React же вызывает функцию-компонент целиком при каждом обновлении родителя, поэтому:

  • Все переменные пересоздаются.
  • Все функции пересоздаются.
  • Все объекты — новые ссылки.

Именно поэтому в React мемоизация — это оптимизация, а во Vue — встроенное поведение.

Когда мемоизация нужна и во Vue

Несмотря на автоматическую реактивность, мемоизация полезна и во Vue:

  • computed() для дорогостоящих вычислений — аналог useMemo.
  • shallowRef() для больших объектов, чтобы избежать глубокого отслеживания.
  • v-memo (директива Vue 3.2+) — мемоизация частей шаблона:
<div v-memo="[selectedItem.id]">
  <ExpensiveComponent :item="selectedItem" />
</div>

Типичные ошибки

1. Inline-объекты и массивы в JSX

// ❌ style — новый объект при каждом рендере, React.memo бесполезен
<MemoizedChild style={{ color: "red" }} />

// ✅ Вынести в useMemo или константу вне компонента
const style = useMemo(() => ({ color: "red" }), []);
<MemoizedChild style={style} />

2. Inline-функции в JSX

// ❌ Новая функция при каждом рендере
<MemoizedChild onClick={() => doSomething(id)} />

// ✅ useCallback + передача как пропс
const handleClick = useCallback(() => doSomething(id), [id]);
<MemoizedChild onClick={handleClick} />

3. Мемоизация примитивов

// ❌ Бессмысленно: примитивы сравниваются по значению, а не по ссылке
const count = useMemo(() => someValue + 1, [someValue]);

// ✅ Просто вычисляйте напрямую
const count = someValue + 1;

useMemo не нужен для сложения, конкатенации строк и других дешёвых операций — стоимость мемоизации (сравнение зависимостей, хранение кэша) превышает стоимость вычисления.

4. Мемоизация без React.memo у дочернего компонента

// ❌ Бесполезно: Child не обёрнут в React.memo,
// поэтому перерендерится в любом случае
const data = useMemo(() => computeData(), []);
<Child data={data} />

Мемоизация пропсов имеет смысл только если дочерний компонент сам мемоизирован через React.memo.

5. Неполный массив зависимостей

// ❌ Баг: userId используется, но не указан в зависимостях
const data = useMemo(() => fetchData(userId), []);

// ✅ Правильно
const data = useMemo(() => fetchData(userId), [userId]);

Линтер eslint-plugin-react-hooks (exhaustive-deps) автоматически ловит эту ошибку — не отключайте его.

6. Мемоизация мутабельных значений

// ❌ Мутация объекта внутри useMemo — кэш вернёт мутированный объект
const config = useMemo(() => {
  const obj = { theme: "dark" };
  obj.theme = currentUser.preference; // мутация
  return obj;
}, [currentUser]);

// ✅ Создавайте новый объект
const config = useMemo(() => ({
  theme: currentUser.preference,
}), [currentUser]);

Когда мемоизация НЕ нужна

Мемоизация — это не бесплатная оптимизация. У неё есть стоимость:

  • Память: каждый мемоизированный результат хранится в кэше.
  • Сравнение зависимостей: React сравнивает текущие и предыдущие зависимости при каждом рендере.
  • Сложность кода: больше обёрток — сложнее читать и отлаживать.

Мемоизация избыточна, когда:

  1. Вычисление дешёвое. Сложение, конкатенация, простые преобразования — не оборачивайте в useMemo.

  2. Компонент рендерится редко. Страница настроек, модальное окно — оптимизация не нужна.

  3. Компонент рендерится быстро. Если рендер занимает <1 мс, мемоизация добавит больше накладных расходов, чем сэкономит.

  4. Нет дочерних компонентов с React.memo. Мемоизация пропсов бессмысленна, если дочерний компонент всё равно перерендерится.

  5. Данные часто меняются. Если зависимости меняются при каждом рендере, мемоизация только добавляет оверхед без выигрыша.

Как определить, нужна ли мемоизация

Используйте React DevTools Profiler:

  1. Запишите профиль взаимодействия.
  2. Найдите компоненты с частыми или долгими рендерами.
  3. Оптимизируйте только те, что действительно замедляют UI.

Правило: не оптимизируйте наперёд. Сначала измерьте, потом оптимизируйте.


React Compiler — автоматическая мемоизация

React Compiler (ранее React Forget, стабилен с 2025 года) — это компилятор, который на этапе сборки автоматически мемоизирует компоненты и выражения. Он анализирует код и вставляет эквиваленты useMemo, useCallback и React.memo туда, где это необходимо.

Что делает компилятор

// Исходный код (без ручной мемоизации)
function ProductCard({ product, onSelect }) {
  const discount = product.price * 0.2;
  const finalPrice = product.price - discount;

  return (
    <div onClick={() => onSelect(product.id)}>
      <h3>{product.name}</h3>
      <p>${finalPrice}</p>
    </div>
  );
}

// Что генерирует компилятор (упрощённо)
function ProductCard({ product, onSelect }) {
  const t0 = _c(6);
  let discount, finalPrice, t1, t2;
  if (t0[0] !== product) {
    discount = product.price * 0.2;
    finalPrice = product.price - discount;
    t1 = () => onSelect(product.id);
    t2 = <div onClick={t1}><h3>{product.name}</h3><p>${finalPrice}</p></div>;
    t0[0] = product; t0[1] = discount; t0[2] = finalPrice; t0[3] = t1; t0[4] = t2;
  } else {
    discount = t0[1]; finalPrice = t0[2]; t1 = t0[3]; t2 = t0[4];
  }
  return t2;
}

Ограничения компилятора

Компилятор не сможет мемоизировать, если:

  • Мутации вместо иммутабельности:

    // ❌ Компилятор не сможет корректно мемоизировать
    const config = { theme: "dark" };
    config.theme = userPref; // мутация
    
    // ✅ Компилятор справится
    const config = { theme: userPref };
  • Нечистые функции с побочными эффектами внутри мемоизируемых выражений.

  • Сторонние библиотеки, несовместимые с правилами компилятора.

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

  • Новые проекты: включайте React Compiler сразу, не пишите ручную мемоизацию.
  • Legacy-проекты: ручная мемоизация всё ещё актуальна — не удаляйте существующий useMemo/useCallback, но и не добавляйте новый без необходимости.
  • Собеседования: знание ручной мемоизации по-прежнему требуется — многие кодовые базы ещё не мигрировали на компилятор.

Шпаргалка

useMemo

const value = useMemo(() => expensiveComputation(a, b), [a, b]);
  • Кэширует значение (результат вычисления).
  • Пересчитывает при изменении зависимостей.
  • Vue-аналог: computed().

useCallback

const fn = useCallback(() => doSomething(a), [a]);
  • Кэширует функцию (ссылку).
  • Эквивалент useMemo(() => fn, deps).
  • Vue-аналог: нет (функции во Vue стабильны по умолчанию).

React.memo

const Memoized = React.memo(Component);
  • Пропускает ререндер компонента, если пропсы не изменились.
  • Поверхностное сравнение пропсов по умолчанию.
  • Vue-аналог: поведение по умолчанию.

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

ЗадачаИнструмент
Дорогостоящее вычислениеuseMemo
Стабильная ссылка на функцию для дочернего компонентаuseCallback
Пропуск ререндера дочернего компонентаReact.memo
Стабильный объект/массив в пропсахuseMemo
Стабильная функция в useEffect зависимостяхuseCallback
Примитивные значенияНичего не нужно
Дешёвые вычисленияНичего не нужно

Связь трёх механизмов

useMemo    → стабильные данные  ↘
                                → React.memo → пропуск ререндера
useCallback → стабильные функции ↗

Разорвать цепочку может любая немемоизированная ссылка в пропсах: inline-объект, inline-функция или немемоизированный дочерний компонент.


Ключевые тезисы для интервью

  • useMemo кэширует результат вычисления; пересчитывает при изменении зависимостей. Vue-аналог: computed().
  • useCallback кэширует ссылку на функцию; эквивалентен useMemo(() => fn, deps). Во Vue не нужен — функции стабильны.
  • React.memo пропускает ререндер компонента, если пропсы не изменились (shallow compare). Во Vue — поведение по умолчанию.
  • Цепочка мемоизации: useMemo → стабильный объект, useCallback → стабильная функция, React.memo → пропуск ререндера.
  • Inline-объекты и inline-функции в JSX ломают React.memo — нужна мемоизация или вынос в константы.
  • Мемоизация примитивов бессмысленна — они сравниваются по значению, а не по ссылке.
  • Мемоизация без React.memo у дочернего компонента бесполезна.
  • React Compiler (2025) автоматизирует мемоизацию — ручная оптимизация уходит в прошлое.
  • Не оптимизируйте наперёд: сначала измерьте через Profiler, потом оптимизируйте.

Заключение

Мемоизация — ключевая техника оптимизации React-приложений. Три механизма (useMemo, useCallback, React.memo) работают в связке: стабильные данные и функции позволяют React.memo пропускать ререндеры дочерних компонентов. Однако мемоизация имеет стоимость (память, сравнение зависимостей, сложность кода), поэтому применяйте её только после измерения производительности. С появлением React Compiler ручная мемоизация в значительной степени автоматизирована.

Полезные ссылки