Утечки памяти в TanStack Query почти никогда не связаны с «сырой»
утечкой JavaScript-объектов в классическом смысле. Основная проблема
заключается в долгоживущем кеше QueryClient, накоплении
QueryObserver, а также в некорректном управлении временем
жизни запросов и подписок на данные.
В основе системы лежит реестр запросов (query cache), где каждый
запрос идентифицируется queryKey. Каждый активный
useQuery создаёт наблюдателя (observer), который
подписывается на изменения кеша. Пока существует хотя бы одна подписка
или запрос не считается «собранным мусором», данные остаются в
памяти.
Каждый query проходит несколько состояний:
gcTimeКлючевая проблема возникает на этапе перехода в inactive состояние. Даже если компонент размонтирован, query может оставаться в кеше, если:
gcTime (старое cacheTime) слишком
великПараметр gcTime определяет, сколько query остаётся в
памяти после потери последнего подписчика.
Если значение завышено или оставлено по умолчанию в приложении с большим количеством динамических запросов, происходит постепенное накопление объектов:
Особенно заметно это в SPA с навигацией между страницами, где каждый переход создаёт новые ключи.
Проблема усиливается при использовании динамических ключей вида:
useQuery({
queryKey: ['items', filters],
queryFn: fetchItems
})
Если filters создаются как новый объект каждый рендер,
cache начинает раздуваться бесконечно.
Одной из самых частых причин утечек является нестабильность ключей.
Пример проблемного поведения:
const filters = { status: 'active' }
useQuery({
queryKey: ['users', filters],
queryFn: fetchUsers
})
Если filters пересоздаётся на каждом рендере, даже с
одинаковыми значениями, создаются разные ссылки. TanStack Query
воспринимает это как новый запрос.
Результат:
Решение заключается в стабилизации ключей через примитивы или мемоизацию структуры.
Каждый useQuery создаёт QueryObserver,
который подписывается на QueryCache.
Если компонент не корректно размонтируется или остаются внешние ссылки на observer, происходит удержание памяти.
Типичные сценарии:
queryClient.getQueryCache().subscribeuseQueryДаже один «зависший» observer удерживает весь query graph, включая данные и метаинформацию.
React Strict Mode в development режиме может вызывать двойное монтирование компонентов. Это приводит к:
Хотя TanStack Query умеет корректно очищать большинство таких случаев, утечки возникают при:
queryFnsetQueryData позволяет вручную записывать данные в кеш.
Проблема возникает, когда в кеш сохраняются ссылки на большие
структуры:
queryClient.setQueryData(['data'], (old) => {
return {
...old,
hugeField: window.someBigObject
}
})
Если такие данные попадают в кеш, они удерживаются до удаления query, а иногда дольше — если на них есть внешние ссылки.
Постоянные refetchInterval создают непрерывную
активность:
Это особенно критично для:
useQuery({
queryKey: ['metrics'],
queryFn: fetchMetrics,
refetchInterval: 5000
})
Если таких запросов десятки или сотни, память стабильно растёт.
TanStack Query использует глобальные listeners:
При неправильной инициализации QueryClient в нескольких
экземплярах приложения могут возникнуть:
Типичный анти-паттерн:
new QueryClient() внутри компонентаuseInfiniteQuery хранит массив страниц
(pages), который может расти бесконтрольно:
getNextPageParamuseInfiniteQuery({
queryKey: ['feed'],
queryFn: fetchFeed,
getNextPageParam: (lastPage) => lastPage.nextCursor
})
Если cursor всегда возвращает новое значение без ограничения, pages продолжают расти, увеличивая память линейно.
При SSR гидратации возможны утечки, если:
Особенно критично при:
Старые данные могут оставаться в памяти даже после смены маршрута.
React Query Devtools создают постоянные подписки на cache:
В production это обычно отключено, но в development может создавать значительное потребление памяти при большом количестве запросов.
Некорректная стратегия invalidation может приводить к:
Пример:
queryClient.invalidateQueries({ queryKey: ['items'] })
При частом вызове без debounce это приводит к лавинообразному росту активности кеша.
Retry механизм также влияет на память:
При retry: Infinity или высокой частоте retry возможен
рост памяти в нестабильных сетевых условиях.
Наиболее частые источники в реальных приложениях:
Некоторые архитектуры усиливают утечки:
В таких системах даже небольшие утечки на уровне observer становятся накопительными и приводят к росту потребления памяти со временем.