Мемоизация в React: useMemo, useCallback и React.memo
Мемоизация — одна из ключевых техник оптимизации производительности в React. Три механизма — useMemo, useCallback и React.memo — решают единую задачу: избежать повторных вычислений и ререндеров, когда входные данные не изменились. С появлением React Compiler (стабильный в 2025 году) ручная мемоизация в значительной степени автоматизирована, но понимание механики остаётся критически важным — для чтения чужого кода, работы с legacy-проектами и осознанного проектирования компонентов.
Содержание
- Что такое мемоизация и зачем она нужна
- useMemo — мемоизация значений
- useCallback — мемоизация функций
- React.memo — мемоизация компонентов
- Как useMemo, useCallback и React.memo работают вместе
- Vue-аналоги
- Типичные ошибки
- Когда мемоизация НЕ нужна
- React Compiler — автоматическая мемоизация
- Шпаргалка
- Ключевые тезисы для интервью
- Заключение
Что такое мемоизация и зачем она нужна
Мемоизация — это кэширование результата выполнения функции для определённого набора входных аргументов. При повторном вызове с теми же аргументами возвращается сохранённый результат без повторного вычисления.
В React каждый ререндер компонента — это вызов функции-компонента. При ререндере:
- Пересоздаются все локальные переменные и объекты.
- Пересоздаются все функции-обработчики.
- Дочерние компоненты получают новые ссылки на пропсы, даже если значения не изменились.
Без мемоизации это приводит к двум проблемам:
- Дорогостоящие вычисления выполняются при каждом рендере, даже если данные не изменились.
- Дочерние компоненты перерендериваются при каждом рендере родителя, даже если их пропсы не изменились — из-за новых ссылок на объекты и функции.
Vue-аналог: во Vue реактивность построена на Proxy, и компонент перерендеривается только при изменении конкретных реактивных значений, используемых в шаблоне. Функции-обработчики не пересоздаются при каждом ререндере, потому что компилятор шаблонов Vue генерирует эффективный рендер-код. В React же компонент — это обычная JS-функция, вызываемая заново при каждом обновлении, поэтому всё внутри неё пересоздаётся.
useMemo — мемоизация значений
useMemo кэширует результат вычисления и пересчитывает только при изменении зависимостей.
Vue-аналог:
computed()— прямой эквивалент. Оба кэшируют результат и пересчитывают при изменении зависимостей. Ключевое отличие: Vuecomputedавтоматически отслеживает зависимости через 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-аналоги
| React | Vue | Описание |
|---|---|---|
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 сравнивает текущие и предыдущие зависимости при каждом рендере.
- Сложность кода: больше обёрток — сложнее читать и отлаживать.
Мемоизация избыточна, когда:
-
Вычисление дешёвое. Сложение, конкатенация, простые преобразования — не оборачивайте в
useMemo. -
Компонент рендерится редко. Страница настроек, модальное окно — оптимизация не нужна.
-
Компонент рендерится быстро. Если рендер занимает <1 мс, мемоизация добавит больше накладных расходов, чем сэкономит.
-
Нет дочерних компонентов с
React.memo. Мемоизация пропсов бессмысленна, если дочерний компонент всё равно перерендерится. -
Данные часто меняются. Если зависимости меняются при каждом рендере, мемоизация только добавляет оверхед без выигрыша.
Как определить, нужна ли мемоизация
Используйте React DevTools Profiler:
- Запишите профиль взаимодействия.
- Найдите компоненты с частыми или долгими рендерами.
- Оптимизируйте только те, что действительно замедляют 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 ручная мемоизация в значительной степени автоматизирована.