React caching стратегии: как выбрать слой кэша и не сломать данные
Разбираем React caching стратегии: memoization, query cache, HTTP cache, SSR/RSC cache, invalidation, performance trade-off и типичные production-ошибки.
- Введение
- Что вообще считается кэшем в React-приложении
- 1. Render cache
- 2. Data cache
- 3. HTTP и browser cache
- 4. Server/framework cache
- Почему одна стратегия почти никогда не покрывает все
- Архитектурный разбор: как слои кэша делят ответственность
- Схема компонентов и слоев
- Поток запроса и обновления
- Где проходит граница ответственности
- Базовый пример: render cache не равен data cache
- Практический пример: query cache с контролируемой stale политикой
- Когда browser cache полезнее, чем еще один React-слой
- Таблица: как выбрать React caching стратегию
- Production pitfalls: где React caching стратегии чаще всего ломают
- Ошибка 1. Копируют server state в локальный store
- Ошибка 2. Слишком большой staleTime без бизнес-обоснования
- Ошибка 3. Лечат сетевую проблему через useMemo
- Ошибка 4. Агрессивно кэшируют без политики invalidation
- Ошибка 5. Игнорируют стоимость памяти
- Разбор производительности: что именно измерять
- 1. Где bottleneck: сеть, CPU или сервер
- 2. Что происходит при первом открытии и при повторном визите
- 3. Какова цена памяти и пересчета
- Практика для server-first приложений: cache на сервере и cache на клиенте
- Best practices: что обычно работает лучше
- Архитектурные практики
- Практики кода
- Практики наблюдаемости
- Практики тестирования
- Rollout и rollback
- Частые ошибки
- Называть любой useMemo "стратегией кэширования"
- Думать, что "чем дольше cache, тем быстрее приложение"
- Смешивать UI state и server state
- Кэшировать без деградации
- Игнорировать связь кэша и рендеров
- Как отвечать на интервью
- FAQ
- Можно ли строить React caching стратегию только на useMemo?
- Когда query cache уже избыточен?
- Нужно ли кэшировать все GET-запросы?
- Чем плоха стратегия "после любой мутации просто инвалидируем все"?
- Как связаны caching стратегии и SSR/RSC?
- Итоги
Введение
React caching стратегии обычно обсуждают слишком плоско: кто-то сводит тему к useMemo, кто-то к React Query, кто-то к HTTP cache, а кто-то вообще к Next.js cache() и revalidate. На практике проблема почти всегда архитектурная. Команда не ошибается в одном API. Она ошибается в том, что помещает разные типы данных в один и тот же слой и потом удивляется stale UI, двойным запросам, лишним ререндерам и непредсказуемой инвалидации.
Если подходить инженерно, у React-приложения почти никогда нет одного кэша. Есть минимум четыре слоя: render-level cache, cache данных, сетевой/browser cache и server/framework cache. Каждый слой ускоряет свой участок pipeline и имеет свою цену. Эта тема хорошо дополняет полный разбор state management в React: как только команда путает источник истины, кэш перестает быть оптимизацией и становится источником расхождений.
Больше вопросов в Telegram
Ежедневные разборы и реальные кейсы с интервью.
Что вообще считается кэшем в React-приложении
Полезно сразу разделить уровни.
1. Render cache
Сюда попадает все, что кэширует вычисления или ссылки внутри рендеринга:
useMemo;useCallback;React.memo;- memoized selectors;
- локальные
MapиWeakMap, если они привязаны к жизненному циклу экрана.
Этот слой не знает ничего про сеть, stale данные и мутации на сервере. Он нужен, чтобы не делать дорогую работу повторно внутри UI-дерева.
2. Data cache
Это слой server state или resource cache:
- TanStack Query;
- SWR;
- собственный resource cache для Suspense;
- framework data cache в server-first приложениях.
Здесь уже важны ключи, дедупликация, stale/fresh политика, refetch, invalidation, optimistic update и обработка ошибок. Если вам нужен разговор про повторные API-запросы и согласованность экрана после мутаций, речь почти всегда идет именно об этом слое. Смежный механизм ожидания UI подробно раскрыт в материале про Suspense для данных.
3. HTTP и browser cache
Этот слой живет ниже React:
Cache-Control;ETag;If-None-Match;- cache статических чанков, картинок и API-ответов на уровне браузера, CDN или reverse proxy.
Он особенно полезен для статики, неизменяемых ассетов и идемпотентных GET-запросов, но он не решает сам по себе, как UI должен реагировать на stale данные, частичные обновления и оптимистические мутации.
4. Server/framework cache
В server-first архитектуре появляется еще один слой:
- SSR cache;
- RSC/data cache;
- route cache;
- full-page cache;
- incremental revalidation.
Это уже разговор не про отдельный компонент, а про стоимость серверного рендера, повторное использование результата на уровне запроса, сегмента или страницы. Если команда работает с server-first моделью, тему полезно изучать вместе с SSR vs CSR vs RSC и hydration в React, потому что выигрыш в серверном кэше легко потерять на тяжелом клиентском слое.
Почему одна стратегия почти никогда не покрывает все
useMemo не заменяет query cache. Query cache не заменяет browser cache. Browser cache не заменяет серверную revalidation. Это разные точки ускорения.
Типичная проблема выглядит следующим образом:
- Команда видит медленные повторные рендеры и добавляет
useMemo. - Затем замечает повторные API-запросы и добавляет query cache.
- После этого включает aggressive CDN caching.
- Потом сталкивается с тем, что после мутации UI показывает старые данные, потому что invalidation rules между слоями не согласованы.
Грамотная React caching стратегия начинается не с библиотеки, а с вопроса: где живет источник истины и кто имеет право объявить данные устаревшими.
Архитектурный разбор: как слои кэша делят ответственность
Возьмем экран аналитики в SaaS-приложении. На нем есть:
- summary cards;
- таблица заказов;
- фильтры по периоду;
- график revenue;
- экспорт CSV;
- кнопка смены статуса заказа.
У этого экрана почти всегда не один кэш, а несколько.
Схема компонентов и слоев
- browser cache хранит JS-чанки, иконки и часть GET-ответов;
- query cache хранит
orders,summary,revenue; - render cache стабилизирует тяжелые derived values, например агрегацию строк или конфигурацию графика;
- server/framework cache может переиспользовать результат серверного чтения для SSR/RSC сценария;
- локальный UI state хранит открытые попапы, выбранные строки и состояние фильтров до коммита в URL.
Поток запроса и обновления
- Пользователь открывает экран.
- Shell страницы приходит сразу или после SSR/RSC.
- Query cache проверяет, есть ли свежие данные по ключам
["orders", filters],["summary", filters],["revenue", filters]. - Если данные свежие, UI рендерится без повторного запроса.
- Если данные stale, UI может показать старое значение и параллельно сделать background refetch.
- После мутации заказа invalidation затрагивает не весь экран подряд, а только связанные ключи.
Где проходит граница ответственности
- Render cache отвечает за CPU внутри React-дерева.
- Query cache отвечает за повторное использование данных и согласованность после мутаций.
- HTTP cache отвечает за транспорт и сетевые round-trip.
- Server/framework cache отвечает за ускорение server render path.
Если эти границы не проговорены, команда начинает лечить сетевую проблему мемоизацией, а проблему инвалидации пытается спрятать под большим staleTime.
Базовый пример: render cache не равен data cache
Ниже типичный случай, где useMemo полезен, но только на своем уровне.
import { useMemo } from "react";
type Order = {
id: string;
amount: number;
status: "new" | "paid" | "cancelled";
};
type Props = {
orders: Order[];
};
export function RevenueSummary({ orders }: Props) {
const totalRevenue = useMemo(() => {
return orders
.filter((order) => order.status === "paid")
.reduce((sum, order) => sum + order.amount, 0);
}, [orders]);
return <strong>{totalRevenue.toLocaleString("ru-RU")} ₽</strong>;
}
Что здесь реально кэшируется:
- не HTTP-ответ;
- не список заказов между экранами;
- не серверное состояние;
- только результат вычисления
totalRevenueдля конкретного набораorders.
Это хороший render-level cache. Но если родитель каждый раз получает новый массив из повторного fetch, useMemo не спасет от сетевых затрат. И наоборот: если данные уже лежат в query cache, это не отменяет пользу локальной мемоизации для дорогой агрегации.
Практический пример: query cache с контролируемой stale политикой
Для server state нужен другой уровень абстракции.
import { useQuery } from "@tanstack/react-query";
type OrdersResponse = {
items: Array<{
id: string;
amount: number;
status: string;
}>;
};
async function fetchOrders(projectId: string): Promise<OrdersResponse> {
const response = await fetch(`/api/projects/${projectId}/orders`);
if (!response.ok) {
throw new Error(`Orders request failed: ${response.status}`);
}
return response.json();
}
export function OrdersPanel({ projectId }: { projectId: string }) {
const { data, isFetching, error } = useQuery({
queryKey: ["orders", projectId],
queryFn: () => fetchOrders(projectId),
staleTime: 30_000,
gcTime: 10 * 60_000,
refetchOnWindowFocus: false,
});
if (error) {
return <div>Не удалось загрузить заказы.</div>;
}
if (!data) {
return <div>Загрузка...</div>;
}
return (
<section>
{isFetching ? <small>Обновляем данные...</small> : null}
<ul>
{data.items.map((item) => (
<li key={item.id}>
{item.id} - {item.amount} ₽
</li>
))}
</ul>
</section>
);
}
Здесь появляются вещи, которых не было в useMemo:
queryKey;staleTime;gcTime;- фоновое обновление;
- общий кэш между несколькими потребителями.
Именно это делает query cache самостоятельным архитектурным слоем, а не просто еще одной оптимизацией рендера.
Когда browser cache полезнее, чем еще один React-слой
Иногда команда слишком быстро тянет caching логику в React, хотя проблема уже лучше решается HTTP-слоем.
Хорошие кандидаты для browser/CDN cache:
- versioned JS/CSS bundles;
- изображения и шрифты;
- редко меняющиеся справочники;
- публичные read-only API с четкими
ETagилиmax-age; - preloaded чанки после code splitting и lazy loading.
Плохая идея для чистого browser cache без data layer сверху:
- списки, которые активно мутируются в интерфейсе;
- данные, где важен optimistic update;
- экраны, где stale значение должно быть явно помечено;
- сценарии, где бизнес-логика зависит от тонкой invalidation policy.
Browser cache ускоряет транспорт, но не дает модели того, как React должен вести себя после нажатия "Сохранить", "Удалить" или "Изменить статус".
Таблица: как выбрать React caching стратегию
| Сценарий | Render cache (useMemo, React.memo) | Query cache | HTTP/browser cache | Server/framework cache | Когда выбирать |
|---|---|---|---|---|---|
| Дорогая агрегация списка в одном экране | Да | Иногда | Нет | Нет | Когда bottleneck в CPU рендера, а не в сети |
| Повторные GET-запросы к одним и тем же данным | Нет | Да | Иногда | Иногда | Когда важны дедупликация, stale policy и background refetch |
| Статические чанки, изображения, словари | Нет | Нет | Да | Иногда | Когда данные редко меняются и нужен быстрый transport path |
| SSR/RSC экран с дорогим серверным чтением | Нет | Иногда | Иногда | Да | Когда основная цена лежит в server render path |
| Мутирующий dashboard с optimistic UI | Частично | Да | Недостаточно | Частично | Когда нужно согласованное поведение после мутаций |
| Конфигурация графика или derived props | Да | Нет | Нет | Нет | Когда повторная работа происходит внутри компонента |
Практический вывод из таблицы простой: в production чаще всего выигрывает комбинация слоев, а не один "правильный" кэш.
Прокачай React за 7 дней
20 вопросов и разборов по React Hooks.
Production pitfalls: где React caching стратегии чаще всего ломают
Ошибка 1. Копируют server state в локальный store
Классический анти-паттерн:
- данные пришли из query cache;
- их копируют в Zustand/Redux "для удобства";
- потом частично меняют локально;
- дальше проект уже живет с двумя источниками истины.
Признаки:
- stale UI после возврата на экран;
- спорные баги "после refresh проходит";
- ручные синки между store и query cache;
- непредсказуемые ререндеры.
Последствие: invalidation становится не локальной операцией, а ручной синхронизацией нескольких слоев.
Ошибка 2. Слишком большой staleTime без бизнес-обоснования
Иногда команда маскирует сетевые проблемы так:
- ставит
staleTimeна 10-30 минут; - получает "быстрый" UI;
- потом обнаруживает, что пользователи смотрят на устаревшие цены, статусы и остатки.
Если данные часто меняются, большой staleTime уже не оптимизация, а осознанная потеря консистентности. Такое решение допустимо только если бизнес действительно принимает этот компромисс.
Ошибка 3. Лечат сетевую проблему через useMemo
Это одна из самых частых собеседовательных ловушек. Кандидат видит слово "cache" и начинает говорить про useMemo, хотя проблема в повторных запросах, инвалидации и дедупликации. Сильный ответ всегда сначала определяет слой.
Ошибка 4. Агрессивно кэшируют без политики invalidation
Фраза "сложнее всего не кэшировать, а инвалидировать" в React особенно практична. Если после мутации вы не можете ответить:
- какие ключи устарели;
- какие панели надо refetch;
- можно ли показать stale UI;
- нужен ли optimistic rollback;
то стратегии еще нет. Есть только набор кэшей.
Ошибка 5. Игнорируют стоимость памяти
Кэш ускоряет чтение, но платит памятью:
- большие query cache на тяжелых таблицах;
- долгий
gcTime; - несколько версий одного и того же списка по разным ключам;
- крупные memoized структуры, которые редко переиспользуются.
На слабых устройствах такой "ускоренный" интерфейс может, наоборот, ухудшить поведение из-за давления на память и GC.
Разбор производительности: что именно измерять
Про caching стратегии бессмысленно говорить без измерений. Полезно разделить три разных вопроса.
1. Где bottleneck: сеть, CPU или сервер
Если проблема в долгом API, React.memo ничего не исправит. Если проблема в тяжелом клиентском преобразовании 10 000 строк, HTTP cache тоже не спасет. Если проблема в дорогом SSR/RSC чтении, нужно смотреть на server/framework cache и стоимость последующей hydration.
2. Что происходит при первом открытии и при повторном визите
Кэш почти всегда меняет профиль производительности неравномерно:
- первый визит может остаться таким же дорогим;
- повторный визит становится быстрее;
- после мутации экран может стать сложнее из-за invalidation и background refetch.
Именно поэтому важно измерять не только cold start, но и повторные navigation flows.
3. Какова цена памяти и пересчета
Минимальный набор сигналов:
- network waterfall;
- число дублей одного запроса;
- CPU time на derived calculations;
- частота ререндеров;
- размер кэшируемых объектов;
- время восстановления UI после мутации.
Если нужно понять, где именно React теряет время на клиенте, держите рядом гайд по React performance profiling. Он хорошо показывает, когда у вас реальная проблема кэша, а когда проблема ширины рендера.
Практика для server-first приложений: cache на сервере и cache на клиенте
В server-first мире картина становится сложнее. Допустим, страница отрисована на сервере, а затем клиент гидратируется и продолжает жить своей жизнью.
Тогда легко получить двойное кэширование:
- сервер уже закэшировал данные для первого ответа;
- клиент после mount делает повторный fetch;
- UI формально быстрый, но сеть и память платят дважды.
Зрелая стратегия в таком случае должна отвечать на два вопроса:
- Какие данные должны переиспользоваться между сервером и клиентом?
- Где заканчивается server cache и начинается client cache?
Если на этот слой смотреть поверхностно, проект быстро получает сценарий "быстрый HTML, но тяжелый клиент". Это тот же компромисс, который виден в разборе Server Components: уменьшить клиентский вес можно только если заранее определить, какие данные и какие вычисления вообще не должны уезжать в браузер.
Best practices: что обычно работает лучше
Архитектурные практики
- Разделяйте render cache и data cache как разные инструменты.
- Держите server state ближе к query/resource layer, а не в общем клиентском store.
- Сразу определяйте owner для invalidation: компонент, mutation handler, route segment или серверный revalidator.
- В server-first приложениях проектируйте перенос данных между сервером и клиентом как отдельную часть архитектуры, а не как побочный эффект.
Практики кода
- Давайте ключам кэша стабильную и предсказуемую структуру.
- Не мемоизируйте все подряд: маленькие дешевые вычисления не окупают overhead.
- Не создавайте derived cache поверх уже кэшируемых устаревших (stale) данных без явной причины.
- Не храните копии одного и того же ответа в трех местах.
Практики наблюдаемости
- Логируйте мутации и связанные invalidation paths.
- Смотрите на дубли сетевых запросов после mount, focus и route transition.
- Проверяйте, не показывает ли UI "успешный" stale state после критичной операции.
Практики тестирования
- Пишите сценарии на refetch после mutation.
- Проверяйте optimistic update и rollback.
- Отдельно тестируйте cold cache и warm cache поведение.
Rollout и rollback
- Вводите агрессивные cache policies постепенно.
- Сначала оценивайте эффект на одном экране или сегменте.
- Оставляйте быстрый способ ослабить caching policy без полного отката фичи.
Частые ошибки
Называть любой useMemo "стратегией кэширования"
Это только один частный инструмент внутри render layer.
Думать, что "чем дольше cache, тем быстрее приложение"
Быстрее не всегда значит лучше. Иногда это просто позволяет дольше не замечать устаревшие данные.
Смешивать UI state и server state
Это почти всегда заканчивается ручной синхронизацией и багами на границе мутаций.
Кэшировать без деградации
Если инвалидация перестала работать (или нарушена), нужен понятный fallback: refetch, rollback, soft reset экрана или временное отключение агрессивной политики.
Игнорировать связь кэша и рендеров
Даже идеальный data cache не спасет, если широкий provider или плохая структура подписок заставляет половину дерева ререндериться на каждое обновление. Эта сторона темы хорошо стыкуется с разбором того, когда React действительно перерисовывает компонент.
Как отвечать на интервью
Хороший ответ по теме React caching стратегии обычно строится так:
- Сначала разделяете слои: render cache, data cache, HTTP cache, server/framework cache.
- Затем объясняете, какой слой решает какую проблему.
- После этого проговариваете invalidation и trade-off между latency, памятью и консистентностью.
- В конце приводите кейс: например dashboard, где summary можно держать stale 30 секунд, а статус платежа после мутации нужно инвалидировать сразу.
Короткая формулировка для уровня middle/senior может звучать так:
"Я не рассматриваю React caching как единый механизм. Для меня это набор слоев. useMemo снижает стоимость вычислений в рендере. Query cache управляет server state, дедупликацией и инвалидацией. HTTP cache ускоряет транспорт. В server-first приложении добавляется server/framework cache. Основной вопрос не в том, что кэшировать, а кто и когда объявляет данные устаревшими".
Практика реальных технических собеседований по React
Разберите вопросы по caching, Suspense, SSR/RSC, performance trade-off и state management в формате mock interview и коротких инженерных разборов.
FAQ
Можно ли строить React caching стратегию только на useMemo?
Нет. useMemo полезен для render-level оптимизации, но он не заменяет query cache, HTTP cache, invalidation policy и согласованность server state после мутаций.
Когда query cache уже избыточен?
Когда данные маленькие, живут в пределах одного экрана, не требуют background refetch и не переживают повторные визиты или сложные мутации. В таком случае отдельный data layer может быть дороже, чем проблема.
Нужно ли кэшировать все GET-запросы?
Нет. Кэш полезен там, где есть повторяемое чтение и приемлемый риск временной устарелости. Если данные критичны к актуальности, агрессивный cache без четкой invalidation policy может принести больше вреда, чем пользы.
Чем плоха стратегия "после любой мутации просто инвалидируем все"?
Она работает как грубый safe default, но часто приводит к лишним запросам, миганию UI и потере локальной отзывчивости. Чем крупнее приложение, тем важнее точечная invalidation.
Как связаны caching стратегии и SSR/RSC?
SSR/RSC добавляют еще один слой кэша и еще один источник компромиссов. Можно ускорить серверный ответ, но потом потерять выигрыш на клиентском refetch, hydration cost или лишнем дублировании данных.
Итоги
React caching стратегии работают хорошо только тогда, когда они разделены по слоям и подчинены одной модели владения данными. Самая частая ошибка не в выборе библиотеки, а в смешении render cache, data cache, browser cache и server cache без понятной invalidation policy.
Если держать в голове простое правило "каждый слой ускоряет свой участок и платит своей ценой", решения становятся заметно яснее. Тогда useMemo перестает притворяться query cache, browser cache не подменяет data layer, а SSR/RSC кэш не конфликтует с клиентским поведением экрана.
Больше вопросов в Telegram
Ежедневные разборы и реальные кейсы с интервью.
Автор
Lexicon Team
Читайте также
frontend
React Suspense для данных: как загружать данные без хаоса в loading-state
Подробно разбираем React Suspense для данных: как работает throw promise, где проходят границы ответственности, как сочетать Suspense с кэшем, Error Boundary и что отвечать на собеседовании.
frontend
Next.js vs чистый React: что выбрать под проект и как объяснить выбор на интервью
Сравниваем Next.js и чистый React: архитектура, SSR и RSC, производительность, стоимость разработки, типичные ошибки и критерии выбора для production.
frontend
Когда нужен Next.js: признаки, что фреймворк окупится в проекте
Разбираем, когда нужен Next.js на практике: SEO, SSR, RSC, серверные мутации, стоимость инфраструктуры, типичные ошибки выбора и сильный ответ на интервью.