React memoization: как реально работает оптимизация

Разбираем React memoization на практике: где React.memo, useMemo и useCallback реально экономят время, где создают overhead и как это объяснять на интервью.

21 марта 2026 г.18 минLexicon Team

Введение

React memoization часто обсуждают так, будто это универсальный способ ускорить интерфейс. В реальной разработке все сложнее. Мемоизация не делает приложение быстрым сама по себе. Она лишь меняет стоимость конкретного участка дерева: иногда позволяет React не вызывать компонент повторно, иногда переиспользует уже вычисленное значение, а иногда только добавляет еще один слой сравнения без заметной пользы.

Из-за этого вокруг темы появляется много ложных правил. Одни оборачивают в useCallback каждый обработчик. Другие считают, что React.memo обязан защищать компонент от любых обновлений. Третьи видят любой повторный вызов компонента как баг производительности. Все три подхода обычно упираются в одну проблему: разработчик не понимает, где именно React тратит время и на каком участке memoization вообще может сработать.

Полезно сразу держать рядом базовую модель когда React действительно перерисовывает компонент. Без нее разговор про оптимизацию быстро превращается в ритуал: хуки добавлены, код стал сложнее, а пользовательский сценарий почти не изменился.

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

Больше вопросов в Telegram

Ежедневные разборы и реальные кейсы с интервью.

Подписаться

Что именно в React называется memoization

В повседневной работе под memoization обычно понимают три разных механизма:

  • React.memo пытается пропустить повторный вызов компонента, если его props считаются эквивалентными.
  • useMemo кеширует результат вычисления внутри компонента.
  • useCallback кеширует ссылку на функцию.

Это важно разделять. Очень многие проблемы начинаются с фразы вроде “добавим memoization”, после которой в код летят все три инструмента сразу, хотя задача на самом деле была одна.

React.memo работает на границе компонента. Если родитель отрисовался заново, React может сравнить старые и новые props ребенка и остановиться, не заходя глубже в эту ветку. Но это возможно только если props стабильны в терминах shallow comparison.

useMemo не мешает компоненту ререндериться. Он работает уже внутри вызванного компонента и позволяет не пересчитывать часть данных, если зависимости не изменились.

useCallback еще уже по смыслу. Он нужен не для “ускорения функций”, а для тех мест, где стабильность ссылки на функцию действительно влияет на поведение: memo-ребенок ниже, dependency list эффекта или другого хука.

Отсюда первый практический вывод: memoization почти никогда не лечит архитектуру. Если состояние поднято слишком высоко, если один Context раздает сразу тему, поиск и онлайн-статусы, если родитель на каждый символ пересоздает большой набор props, то мемоизация внизу дерева только маскирует источник проблемы. В таких сценариях полезнее сначала пересмотреть границы состояния, как видно в разборе когда Context API действительно уместен.

Архитектура оптимизации: где React тратит время

Контекст задачи

Когда пользователь печатает в строку поиска, происходит не абстрактный “ререндер”, а цепочка событий:

  1. Меняется локальный state.
  2. React заново вызывает компонент.
  3. Во время этого вызова создаются новые объекты, массивы, функции и JSX.
  4. React сравнивает дерево и решает, какие ветки можно пропустить.
  5. После render-фазы идет commit, затем эффекты и работа браузера.

Memoization влияет только на часть этой цепочки. Она не ускоряет сеть, не чинит layout и paint, не убирает тяжелый commit и не исправляет плохие границы Context.

Схема компонентов

Для React memoization удобно держать такую модель:

  • state и context определяют, какие части дерева обязаны обновиться.
  • React.memo пытается остановить повторный вызов дочернего компонента.
  • useMemo экономит CPU на вычислении или стабилизирует ссылку на результат.
  • useCallback стабилизирует функцию, чтобы она не ломала memo-границу ниже.
  • локализация state и дробление дерева часто дают больший выигрыш, чем сами хуки.

Ключевое слово здесь bailout. Это момент, когда React понимает: в эту ветку дерева можно не заходить глубже. Но bailout возможен не везде. Если в props лежат объекты и функции, React ориентируется на ссылку. Даже два одинаковых по структуре объекта для shallow comparison считаются разными, если созданы заново.

Где обычно все ломается

Самые частые точки отказа выглядят так:

  • родитель каждый раз создает новый объект filters;
  • ребенку передается inline-колбэк;
  • memo-компонент читает часто меняющийся context;
  • useMemo оборачивает тривиальное вычисление и создает overhead без пользы.

Поэтому разговор про memoization почти всегда должен идти рядом с профилированием и с общим пониманием рендеринга. Если хочется видеть картину шире, полезно держать рядом материал про React Fiber и механику render-процесса.

Код-пример 1: почему React.memo часто бесполезен

Ниже типичный случай, где React.memo не дает никакого эффекта, хотя формально “все сделали правильно”:

import React, { useState } from "react";

type UserCardProps = {
  user: { id: string; name: string };
  onOpen: (id: string) => void;
};

const UserCard = React.memo(function UserCard({
  user,
  onOpen,
}: UserCardProps) {
  console.log("UserCard render", user.id);

  return (
    <button onClick={() => onOpen(user.id)}>
      {user.name}
    </button>
  );
});

export function UsersPage() {
  const [query, setQuery] = useState("");

  const user = { id: "42", name: "Ada Lovelace" };

  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />

      <UserCard
        user={user}
        onOpen={(id) => {
          console.log("open", id);
        }}
      />
    </>
  );
}

Проблема здесь не в React.memo, а в том, что props каждый раз новые:

  • объект user создается заново;
  • функция onOpen создается заново;
  • shallow comparison видит новые ссылки;
  • bailout не происходит.

Это очень частый production-кейс: команда видит тяжелый компонент, добавляет React.memo, но не стабилизирует входные данные. В итоге теперь есть и стоимость сравнения props, и полный ререндер. То есть мы платим больше, не получая выгоды.

Код-пример 2: когда memoization действительно оправдана

Теперь пример, где связка из useMemo, useCallback и React.memo закрывает понятную проблему:

import React, { useCallback, useMemo, useState } from "react";

type Product = {
  id: string;
  title: string;
  category: string;
};

type ProductListProps = {
  items: Product[];
  onSelect: (id: string) => void;
};

const ProductList = React.memo(function ProductList({
  items,
  onSelect,
}: ProductListProps) {
  console.log("ProductList render", items.length);

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <button onClick={() => onSelect(item.id)}>{item.title}</button>
        </li>
      ))}
    </ul>
  );
});

export function Catalog({ products }: { products: Product[] }) {
  const [query, setQuery] = useState("");
  const [category, setCategory] = useState("all");

  const filteredProducts = useMemo(() => {
    const normalizedQuery = query.trim().toLowerCase();

    return products.filter((product) => {
      const matchesCategory =
        category === "all" || product.category === category;
      const matchesQuery = product.title.toLowerCase().includes(normalizedQuery);

      return matchesCategory && matchesQuery;
    });
  }, [products, query, category]);

  const handleSelect = useCallback((id: string) => {
    console.log("open product", id);
  }, []);

  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />

      <select value={category} onChange={(e) => setCategory(e.target.value)}>
        <option value="all">All</option>
        <option value="books">Books</option>
        <option value="tools">Tools</option>
      </select>

      <ProductList items={filteredProducts} onSelect={handleSelect} />
    </>
  );
}

Здесь каждый инструмент решает конкретную задачу:

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

Но важно не делать отсюда ложный вывод, будто теперь список обязан не ререндериться при вводе. Если query меняется, filteredProducts меняется честно, и ProductList должен отрисоваться заново. Memoization не отменяет необходимую работу. Она лишь убирает лишнюю.

Сравнение подходов

КритерийБез memoizationReact.memouseMemouseCallback
Что оптимизируетНичего не кешируетПовторный вызов компонентаРезультат вычисленияСсылку на функцию
Где работаетПо умолчаниюНа границе компонентаВнутри компонентаВнутри компонента
Когда полезенЕсли код и так быстрыйЕсли компонент дорогой и props стабильныЕсли вычисление дорогоеЕсли ссылка реально участвует в сравнении
Частая ошибкаПаниковать из-за любого ререндераПередавать новые объекты и функцииКешировать тривиальные вещиОборачивать любой handler
Основная ценаНет дополнительнойСравнение propsСравнение dependencies и хранение кешаСравнение dependencies и усложнение кода
Лучший контекстПростые компонентыТяжелые карточки, списки, строки таблицФильтрация, агрегация, derived dataMemo-дети и стабильные зависимости

Эта таблица полезна как напоминание: в реальной разработке выигрывает не максимальное число оптимизаций, а минимально достаточное.

Production pitfalls

1. Кешируют дешевое, игнорируя дорогое

Часто в кодовой базе можно увидеть useMemo(() => ({ isOpen }), [isOpen]), но рядом на каждый символ запускается тяжелая сортировка или фильтрация большого массива. Это типичная ошибка фокуса: усилия ушли в видимую мелочь, а горячий путь остался без внимания.

Признак простой: код стал заметно сложнее, а пользовательский сценарий почти не ускорился. В таких случаях нужно возвращаться к измерениям, а не добавлять еще один хук. Для системного поиска причины полезен материал про React performance profiling и поиск узких мест.

2. Пишут comparator, который дороже самого рендера

Иногда пытаются “спасти” React.memo кастомной функцией сравнения props. Проблема в том, что глубокая проверка может стоить дороже повторного вызова компонента, особенно если comparator запускается часто.

Это особенно неприятно в таблицах, деревьях и списках: компонент вроде перестал ререндериться, но общее время взаимодействия почти не изменилось, потому что цена просто переехала из render в compare.

3. Забывают, что context пробивает memo-границу

React.memo сравнивает props, но не защищает от изменений context. Если компонент читает провайдер, который обновляется часто, он будет вызываться снова независимо от стабильности props. Именно поэтому слишком широкий Context часто убивает пользу memoization раньше, чем проблема доходит до дочерних компонентов.

4. Ломают корректность ради стабильной ссылки

Самая опасная ошибка связана не с производительностью, а с поведением. Разработчик убирает зависимость из useCallback или useMemo, чтобы сохранить стабильную ссылку, и получает stale closure. Интерфейс вроде стал “спокойнее”, но работает на старых данных. Такие баги особенно болезненны в формах, подписках, сокетах и optimistic UI.

Прокачай React за 7 дней

20 вопросов и разборов по React Hooks.

Начать

Разбор производительности: когда оптимизация окупается

У memoization всегда есть своя цена:

  • нужно хранить предыдущее значение или снимок props;
  • нужно сравнивать зависимости;
  • код становится сложнее читать;
  • растет шанс ошибиться с корректностью.

Поэтому правильный вопрос не “можно ли это мемоизировать”, а “дешевле ли сравнение, чем пересчет”.

Сценарии, где memoization обычно окупается:

  • большая таблица с дорогими строками;
  • длинный список карточек;
  • фильтрация, сортировка и агрегация заметного объема данных;
  • передача props в стабильное поддерево (subtree);
  • интеграции с charting, редакторами, canvas или сложными формами.

Сценарии, где она часто лишняя:

  • компонент короткий и дешевый;
  • данные маленькие;
  • зависимости почти всегда меняются;
  • проблема на самом деле в сети, layout или стороннем коде;
  • выигрыш легче получить переносом state ближе к месту использования.

Отдельный нюанс в том, что быстрый render не всегда означает быстрый UI. Если проблема в commit-фазе или в работе браузера после React, memoization даст слабый эффект. А если узкое место в приоритетах обновлений, полезно понимать и как работает concurrent rendering в React.

Практики, которые работают лучше слепой memoization

Локализуйте state

Чем ниже живет состояние, тем меньше дерева React обязан проходить при обновлении. Очень часто перенос useState ближе к месту использования дает больше эффекта, чем набор новых хуков.

Упрощайте props

Если можно передать id, а не целый объект, чаще всего это лучше. Чем проще контракт между компонентами, тем дешевле сравнение и тем меньше шанс случайно сломать bailout.

Дробите context

Не смешивайте инфраструктурные данные, вроде темы, с горячими данными, которые меняются на каждый ввод. Чем шире контекст, тем дороже его обновление.

Профилируйте до и после

Без этого оптимизация почти всегда превращается в веру. А в командной разработке это еще и источник мусора: в коде остаются useMemo и useCallback, пользу которых уже никто не может объяснить.

Удаляйте лишнюю memoization

Если сценарий изменился и выигрыш исчез, хук нужно убирать. “Пусть полежит, вдруг пригодится” почти всегда превращается в технический долг. Этот тип мышления очень хорошо проверяют и на React optimization interview для middle.

Частые ошибки

  • Считать, что любой ререндер плох сам по себе.
  • Путать render с обязательным обновлением DOM.
  • Ждать, что React.memo защитит от изменений context.
  • Добавлять useCallback каждому обработчику без причины.
  • Кешировать значения, которые дешевле пересчитать заново.
  • Писать глубокий comparator без профилирования.
  • Убирать зависимости из хуков ради “стабильности”.
  • Чинить следствие, когда проблема в структуре state и props.

Как отвечать на интервью

Сильный ответ по теме React memoization почти всегда исходит из механики, а не от определения.

Хорошая структура ответа такая:

  1. Коротко сказать, что именно кешируется.
  2. Объяснить, где React сравнивает по ссылке, а где по значению.
  3. Показать, что у каждого инструмента есть overhead.
  4. Привести пример, где оптимизация оправдана.

Рабочая формулировка может звучать так: memoization в React не ускоряет приложение автоматически. React.memo помогает пропускать повторный вызов компонента при стабильных props, useMemo позволяет не пересчитывать дорогое derived value, useCallback стабилизирует функцию для memo-границ и зависимостей. Но все три инструмента стоят ресурсов, поэтому применять их нужно только после профилирования и только там, где сравнение действительно дешевле пересчета.

Слабый ответ обычно выглядит так: “useMemo и useCallback нужны, чтобы не было лишних ререндеров”. В нем нет ни триггеров, ни ограничений, ни понимания того, что именно считается лишним.

Практика по React ценнее списка терминов

Если хотите уверенно разбирать темы вроде memoization, производительности и ререндеров на реальных технических интервью по React, полезнее всего тренироваться на сценариях с объяснением причин и компромиссов.

Перейти к практике

FAQ

React.memo и useMemo решают одну задачу?

Нет. React.memo работает на уровне компонента и props, а useMemo на уровне значения внутри компонента.

Почему новый объект ломает memoization?

Потому что shallow comparison сравнивает объект по ссылке. Новый объект с теми же полями все равно считается новым значением.

Может ли useCallback ускорить приложение сам по себе?

Редко. Обычно он полезен только как часть связки, где ссылка на функцию действительно влияет на memo-границу или поведение других хуков.

Когда кастомный comparator в React.memo оправдан?

Только когда вы измерили, что повторный рендер дорогой, а логика сравнения действительно дешевле и безопасна.

Что чаще дает больший выигрыш, чем memoization?

Локализация state, упрощение props, дробление context, виртуализация списков и устранение дорогих вычислений в горячем пути.

Итоги

React memoization работает не как магическая кнопка, а как точечный обмен: мы платим за сравнение и хранение прошлого состояния, чтобы в нужный момент не платить за более дорогой пересчет. Если этот пересчет действительно дорогой и входные данные стабилизированы, выгода заметна. Если нет, код становится сложнее без реального ускорения.

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

Больше вопросов в Telegram

Ежедневные разборы и реальные кейсы с интервью.

Подписаться

Автор

Lexicon Team

Читайте также