React caching стратегии: как выбрать слой кэша и не сломать данные

Разбираем React caching стратегии: memoization, query cache, HTTP cache, SSR/RSC cache, invalidation, performance trade-off и типичные production-ошибки.

03 апреля 2026 г.20 минLexicon Team

Введение

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. Это разные точки ускорения.

Типичная проблема выглядит следующим образом:

  1. Команда видит медленные повторные рендеры и добавляет useMemo.
  2. Затем замечает повторные API-запросы и добавляет query cache.
  3. После этого включает aggressive CDN caching.
  4. Потом сталкивается с тем, что после мутации 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.

Поток запроса и обновления

  1. Пользователь открывает экран.
  2. Shell страницы приходит сразу или после SSR/RSC.
  3. Query cache проверяет, есть ли свежие данные по ключам ["orders", filters], ["summary", filters], ["revenue", filters].
  4. Если данные свежие, UI рендерится без повторного запроса.
  5. Если данные stale, UI может показать старое значение и параллельно сделать background refetch.
  6. После мутации заказа 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 cacheHTTP/browser cacheServer/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

Классический анти-паттерн:

  1. данные пришли из query cache;
  2. их копируют в Zustand/Redux "для удобства";
  3. потом частично меняют локально;
  4. дальше проект уже живет с двумя источниками истины.

Признаки:

  • 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 формально быстрый, но сеть и память платят дважды.

Зрелая стратегия в таком случае должна отвечать на два вопроса:

  1. Какие данные должны переиспользоваться между сервером и клиентом?
  2. Где заканчивается 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 стратегии обычно строится так:

  1. Сначала разделяете слои: render cache, data cache, HTTP cache, server/framework cache.
  2. Затем объясняете, какой слой решает какую проблему.
  3. После этого проговариваете invalidation и trade-off между latency, памятью и консистентностью.
  4. В конце приводите кейс: например 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

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