В основе производительности лежит модель кэширования, реализованная в TanStack Query. Главная цель механизма — минимизация количества сетевых обращений при сохранении актуальности данных. Кэш работает как промежуточный слой между UI и сервером, позволяя переиспользовать ранее загруженные результаты без повторного запроса.
Ключевыми параметрами, влияющими на производительность, выступают
staleTime и gcTime.
staleTime определяет период, в течение которого данные считаются свежими. Пока данные не устарели, повторные маунты компонентов или переключения между экранами не инициируют сетевой запрос. Это критично для интерфейсов с частыми переходами между представлениями.
gcTime (garbage collection time) регулирует, как долго неиспользуемый кэш сохраняется в памяти. Слишком малое значение приводит к частым пересозданиям запросов, слишком большое — к росту потребления памяти.
Баланс между этими параметрами формирует поведение системы под нагрузкой: от полностью реактивного до почти статического кэширования.
Одним из ключевых оптимизационных механизмов является дедупликация
запросов. При одновременном запуске нескольких компонентов, использующих
одинаковый queryKey, выполняется только один сетевой
запрос. Все подписчики получают результат из одного источника.
Это устраняет проблему «request waterfall», когда одинаковые запросы выполняются параллельно из разных частей дерева компонентов.
Дополнительно решается проблема гонок (race conditions). Если несколько запросов с одинаковым ключом выполняются последовательно, система гарантирует, что финальное состояние кэша соответствует последнему завершённому запросу, а не первому инициированному.
Производительность кэша напрямую зависит от корректной структуры
queryKey. Он должен быть:
Ошибочная практика — использование объектов без мемоизации:
useQuery({
queryKey: [{ userId }],
queryFn: fetchUser
})
Каждый рендер создаёт новый объект, что приводит к восприятию ключа как уникального и фактически инвалидирует кэш.
Оптимальный вариант — примитивные или стабилизированные структуры:
useQuery({
queryKey: ['user', userId],
queryFn: fetchUser
})
Гранулярность ключа также влияет на повторное использование данных. Слишком общий ключ приводит к «переперекрытию» данных, слишком детальный — к фрагментации кэша и снижению hit-rate.
Функция select позволяет извлекать и трансформировать
только необходимую часть данных без повторного сетевого запроса. Это
снижает нагрузку на UI-слой и уменьшает количество пересозданий
производных структур.
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
select: (data) => data.map(user => user.id)
})
Производительность здесь достигается за счёт двух механизмов:
selectЕсли результат селектора не изменился, компоненты не перерисовываются, что критично при больших списках.
Одним из скрытых, но важных механизмов является structural sharing. При обновлении данных система старается переиспользовать неизменённые части структуры объекта.
Это уменьшает:
Особенно заметно это при работе с массивами и вложенными объектами, где небольшое изменение одной записи не должно приводить к пересозданию всей структуры.
При работе с большими наборами данных ключевым фактором становится
стратегия загрузки. Использование pagination и
useInfiniteQuery позволяет избежать загрузки полного набора
данных.
Основная идея — ограничение объёма активного кэша и сетевой нагрузки.
При классической пагинации каждый page хранится
отдельно, что уменьшает риск перераздувания состояния. Однако при
неправильной реализации возможна избыточная фрагментация кэша.
useInfiniteQuery решает задачу агрегации страниц, но
увеличивает риск роста памяти при бесконтрольном накоплении результатов.
Для компенсации используется:
Prefetching позволяет заранее загружать данные, которые с высокой вероятностью понадобятся пользователю. Это снижает perceived latency — время до отображения результата.
Типичный сценарий — загрузка данных при наведении на ссылку или при появлении элемента в viewport.
queryClient.prefetchQuery({
queryKey: ['product', id],
queryFn: fetchProduct
})
Эффект предзагрузки особенно заметен в интерфейсах с навигацией между сущностями, где задержка между действиями пользователя и сетевым ответом становится нулевой с точки зрения UX.
Инвалидация — одна из самых дорогих операций в системе. При вызове
invalidateQueries происходит:
Неправильная стратегия инвалидации приводит к эффекту «штормового обновления», когда один mutation вызывает десятки запросов.
Оптимизация достигается через:
Производительность сильно зависит от стабильности
queryFn и параметров запроса. При пересоздании функций на
каждом рендере возможны лишние пересчёты и нарушения кэш-хитов.
const fetchUser = useCallback(() => {
return api.getUser(userId)
}, [userId])
Стабильные ссылки уменьшают вероятность повторной инициализации запроса и упрощают сравнение зависимостей внутри системы кэширования.
Механизм автоматических повторов увеличивает устойчивость, но может негативно влиять на производительность при деградации сети.
Без ограничений retry приводит к:
Оптимизация включает:
При серверном рендеринге ключевым фактором становится передача состояния кэша на клиент. Гидратация позволяет избежать повторных запросов при первом рендере.
Основные проблемы производительности возникают при:
Оптимизация заключается в минимизации гидратируемого состояния и строгой синхронизации ключей.
При длительной работе приложения кэш может становиться источником утечек памяти. Основные причины:
Решение базируется на:
gcTimeСистема автоматически группирует обновления состояния, уменьшая количество реактивных перерисовок. Это особенно важно при массовых инвалидациях или одновременных мутациях.
Эффект батчинга снижает:
Механизмы временного отображения данных уменьшают визуальные задержки.
placeholderData позволяет показывать структуру данных до
завершения загрузки, а keepPreviousData сохраняет
предыдущий результат между переходами.
Это снижает perceived loading time и предотвращает «мигание» интерфейса при смене queryKey.
Основной эффект заключается в стабилизации UI при изменении параметров запроса, особенно в фильтрах и пагинации.