Производительность запросов

В основе производительности лежит модель кэширования, реализованная в TanStack Query. Главная цель механизма — минимизация количества сетевых обращений при сохранении актуальности данных. Кэш работает как промежуточный слой между UI и сервером, позволяя переиспользовать ранее загруженные результаты без повторного запроса.

Ключевыми параметрами, влияющими на производительность, выступают staleTime и gcTime.

staleTime определяет период, в течение которого данные считаются свежими. Пока данные не устарели, повторные маунты компонентов или переключения между экранами не инициируют сетевой запрос. Это критично для интерфейсов с частыми переходами между представлениями.

gcTime (garbage collection time) регулирует, как долго неиспользуемый кэш сохраняется в памяти. Слишком малое значение приводит к частым пересозданиям запросов, слишком большое — к росту потребления памяти.

Баланс между этими параметрами формирует поведение системы под нагрузкой: от полностью реактивного до почти статического кэширования.


Дедупликация запросов и предотвращение гонок

Одним из ключевых оптимизационных механизмов является дедупликация запросов. При одновременном запуске нескольких компонентов, использующих одинаковый queryKey, выполняется только один сетевой запрос. Все подписчики получают результат из одного источника.

Это устраняет проблему «request waterfall», когда одинаковые запросы выполняются параллельно из разных частей дерева компонентов.

Дополнительно решается проблема гонок (race conditions). Если несколько запросов с одинаковым ключом выполняются последовательно, система гарантирует, что финальное состояние кэша соответствует последнему завершённому запросу, а не первому инициированному.


Структура queryKey и влияние на кеш-эффективность

Производительность кэша напрямую зависит от корректной структуры queryKey. Он должен быть:

  • стабильным (не пересоздаваться на каждом рендере)
  • детерминированным (одни и те же входные данные дают одинаковый ключ)
  • минимально гранулярным без потери семантики

Ошибочная практика — использование объектов без мемоизации:

useQuery({
  queryKey: [{ userId }],
  queryFn: fetchUser
})

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

Оптимальный вариант — примитивные или стабилизированные структуры:

useQuery({
  queryKey: ['user', userId],
  queryFn: fetchUser
})

Гранулярность ключа также влияет на повторное использование данных. Слишком общий ключ приводит к «переперекрытию» данных, слишком детальный — к фрагментации кэша и снижению hit-rate.


Select и минимизация вычислений на стороне клиента

Функция select позволяет извлекать и трансформировать только необходимую часть данных без повторного сетевого запроса. Это снижает нагрузку на UI-слой и уменьшает количество пересозданий производных структур.

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  select: (data) => data.map(user => user.id)
})

Производительность здесь достигается за счёт двух механизмов:

  • мемоизация результата select
  • структурное сравнение предыдущего и нового значения

Если результат селектора не изменился, компоненты не перерисовываются, что критично при больших списках.


Структурное разделение данных и structural sharing

Одним из скрытых, но важных механизмов является structural sharing. При обновлении данных система старается переиспользовать неизменённые части структуры объекта.

Это уменьшает:

  • количество аллокаций памяти
  • нагрузку на garbage collector
  • число ререндеров в React-дереве

Особенно заметно это при работе с массивами и вложенными объектами, где небольшое изменение одной записи не должно приводить к пересозданию всей структуры.


Пагинация и infinite queries как фактор производительности

При работе с большими наборами данных ключевым фактором становится стратегия загрузки. Использование pagination и useInfiniteQuery позволяет избежать загрузки полного набора данных.

Основная идея — ограничение объёма активного кэша и сетевой нагрузки.

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

useInfiniteQuery решает задачу агрегации страниц, но увеличивает риск роста памяти при бесконтрольном накоплении результатов. Для компенсации используется:

  • ограничение количества страниц в памяти
  • виртуализация списков
  • очистка устаревших страниц через invalidation

Prefetching и скрытая оптимизация времени отклика

Prefetching позволяет заранее загружать данные, которые с высокой вероятностью понадобятся пользователю. Это снижает perceived latency — время до отображения результата.

Типичный сценарий — загрузка данных при наведении на ссылку или при появлении элемента в viewport.

queryClient.prefetchQuery({
  queryKey: ['product', id],
  queryFn: fetchProduct
})

Эффект предзагрузки особенно заметен в интерфейсах с навигацией между сущностями, где задержка между действиями пользователя и сетевым ответом становится нулевой с точки зрения UX.


Invalidation и стоимость обновления кэша

Инвалидация — одна из самых дорогих операций в системе. При вызове invalidateQueries происходит:

  • пометка ключей как устаревших
  • триггер повторных запросов
  • потенциальная каскадная цепочка перерисовок

Неправильная стратегия инвалидации приводит к эффекту «штормового обновления», когда один mutation вызывает десятки запросов.

Оптимизация достигается через:

  • точечную инвалидацию ключей
  • использование предсказуемых queryKey схем
  • разделение read/write моделей

Меморизация и стабильность функций

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

const fetchUser = useCallback(() => {
  return api.getUser(userId)
}, [userId])

Стабильные ссылки уменьшают вероятность повторной инициализации запроса и упрощают сравнение зависимостей внутри системы кэширования.


Retry стратегия и влияние на нагрузку

Механизм автоматических повторов увеличивает устойчивость, но может негативно влиять на производительность при деградации сети.

Без ограничений retry приводит к:

  • лавинообразной нагрузке на API
  • блокировке UI из-за повторных состояний loading
  • увеличению времени до финального ответа

Оптимизация включает:

  • ограничение числа попыток
  • экспоненциальную задержку
  • отключение retry для неидемпотентных запросов

SSR и гидратация кэша

При серверном рендеринге ключевым фактором становится передача состояния кэша на клиент. Гидратация позволяет избежать повторных запросов при первом рендере.

Основные проблемы производительности возникают при:

  • избыточной сериализации кэша
  • передаче больших объектов состояния
  • несогласованности queryKey между сервером и клиентом

Оптимизация заключается в минимизации гидратируемого состояния и строгой синхронизации ключей.


Память и долгоживущие кэши

При длительной работе приложения кэш может становиться источником утечек памяти. Основные причины:

  • отсутствие gc стратегии для редко используемых ключей
  • накопление infinite queries без ограничения
  • хранение больших структур без нормализации

Решение базируется на:

  • корректной настройке gcTime
  • разделении активных и фоновых данных
  • периодической инвалидации редко используемых ключей

Батчинг обновлений и снижение количества ререндеров

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

Эффект батчинга снижает:

  • нагрузку на React reconciliation
  • количество вызовов селекторов
  • частоту обновления DOM

Оптимизация через placeholderData и keepPreviousData

Механизмы временного отображения данных уменьшают визуальные задержки.

placeholderData позволяет показывать структуру данных до завершения загрузки, а keepPreviousData сохраняет предыдущий результат между переходами.

Это снижает perceived loading time и предотвращает «мигание» интерфейса при смене queryKey.

Основной эффект заключается в стабилизации UI при изменении параметров запроса, особенно в фильтрах и пагинации.