Медленные запросы в приложениях с TanStack Query могут появляться по множеству причин. Проблема не всегда связана непосредственно с сервером или сетью. Значительная часть задержек возникает из-за неправильной архитектуры клиентского кеша, неудачной конфигурации запросов, чрезмерного количества перерисовок компонентов или ошибок в механизмах синхронизации данных.
Основные источники проблем:
queryKey;refetch;Диагностика должна охватывать одновременно:
Пакет Devtools предоставляет встроенные инструменты анализа запросов.
Установка:
npm install @tanstack/react-query-devtools
Подключение:
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
function App() {
return (
<>
<Application />
<ReactQueryDevtools initialIsOpen={false} />
</>
)
}
Devtools позволяют анализировать:
Особенно полезен просмотр:
Last UpdatedObserversData ExplorerQuery HashЕсли один и тот же запрос создаётся десятки раз с разными ключами,
проблема часто связана с нестабильным queryKey.
Пример ошибки:
useQuery({
queryKey: ['users', { page: currentPage }],
queryFn: fetchUsers
})
Если объект создаётся заново при каждом рендере, может происходить постоянная генерация новых ключей.
Безопаснее использовать примитивы:
useQuery({
queryKey: ['users', currentPage],
queryFn: fetchUsers
})
Для диагностики полезно измерять длительность выполнения запросов.
Простейший способ:
const fetchUsers = async () => {
const start = performance.now()
const response = await fetch('/api/users')
const data = await response.json()
const end = performance.now()
console.log(`Request time: ${end - start}ms`)
return data
}
Более удобный подход — создание общего HTTP-клиента.
Пример с axios:
import axios from 'axios'
const api = axios.create({
baseURL: '/api'
})
api.interceptors.request.use(config => {
config.metadata = {
startTime: performance.now()
}
return config
})
api.interceptors.response.use(response => {
const endTime = performance.now()
const duration =
endTime - response.config.metadata.startTime
console.log(
`${response.config.url}: ${duration.toFixed(2)}ms`
)
return response
})
Такой подход позволяет:
Одной из самых распространённых проблем являются waterfall-запросы — последовательные запросы, блокирующие друг друга.
Плохой пример:
const userQuery = useQuery({
queryKey: ['user'],
queryFn: fetchUser
})
const postsQuery = useQuery({
queryKey: ['posts', userQuery.data?.id],
queryFn: () => fetchPosts(userQuery.data.id),
enabled: !!userQuery.data
})
Здесь второй запрос стартует только после завершения первого.
Если API позволяет, предпочтительнее объединять данные:
const dashboardQuery = useQuery({
queryKey: ['dashboard'],
queryFn: fetchDashboard
})
Или выполнять параллельные запросы:
const results = useQueries({
queries: [
{
queryKey: ['users'],
queryFn: fetchUsers
},
{
queryKey: ['posts'],
queryFn: fetchPosts
}
]
})
Waterfall особенно опасны:
TanStack Query автоматически переиспользует кеш, однако ошибки конфигурации могут приводить к бесконечным refetch.
Типичный пример:
useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
staleTime: 0
})
При staleTime: 0 данные моментально считаются
устаревшими.
Следствия:
Более безопасная конфигурация:
useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
staleTime: 1000 * 60 * 5
})
Дополнительные параметры:
useQuery({
queryKey: ['profile'],
queryFn: fetchProfile,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false
})
Каждый useQuery создаёт observer.
Если десятки компонентов подписаны на один и тот же запрос, возможны:
Пример проблемной архитектуры:
function UserName() {
const query = useQuery({
queryKey: ['user'],
queryFn: fetchUser
})
return <div>{query.data.name}</div>
}
function UserAvatar() {
const query = useQuery({
queryKey: ['user'],
queryFn: fetchUser
})
return <img src={query.data.avatar} />
}
Несмотря на общий кеш, observers всё равно создаются.
Оптимизация:
function UserContainer() {
const query = useQuery({
queryKey: ['user'],
queryFn: fetchUser
})
return (
<>
<UserName user={query.data} />
<UserAvatar user={query.data} />
</>
)
}
select позволяет преобразовывать данные перед передачей
в компонент.
Проблемный пример:
useQuery({
queryKey: ['products'],
queryFn: fetchProducts,
select: data => {
return data
.filter(product => product.active)
.sort((a, b) => b.price - a.price)
.map(product => ({
...product,
formattedPrice: `$${product.price}`
}))
}
})
Если массив содержит тысячи элементов, вычисления становятся дорогими.
Решения:
Использование useMemo:
const query = useQuery({
queryKey: ['products'],
queryFn: fetchProducts
})
const products = useMemo(() => {
return query.data?.filter(p => p.active)
}, [query.data])
Нестабильные ключи — одна из наиболее скрытых причин деградации производительности.
Проблема:
useQuery({
queryKey: ['search', filters],
queryFn: fetchSearch
})
Если filters создаётся заново:
const filters = {
category,
sort
}
то TanStack Query может воспринимать ключ как новый.
Решение:
const filters = useMemo(() => ({
category,
sort
}), [category, sort])
Или:
queryKey: ['search', category, sort]
Повторные попытки могут существенно замедлять интерфейс.
Стандартное поведение:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
TanStack Query выполняет retries автоматически.
Если сервер отвечает ошибками, время ожидания возрастает:
1 попытка → ошибка
retry delay
2 попытка → ошибка
retry delay
3 попытка → ошибка
Для диагностики полезно временно отключать retries:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
retry: false
})
Или ограничивать:
retry: 1
Глобальное состояние загрузки помогает выявлять скрытые запросы.
Пример:
import { useIsFetching } fr om '@tanstack/react-query'
function GlobalLoader() {
const isFetching = useIsFetching()
return (
<div>
Active requests: {isFetching}
</div>
)
}
Если значение постоянно больше нуля, возможны:
Неконтролируемая инвалидизация кеша часто становится причиной перегрузки API.
Плохой пример:
queryClient.invalidateQueries()
Такой вызов инвалидирует весь кеш приложения.
Следствия:
Предпочтительнее:
queryClient.invalidateQueries({
queryKey: ['users']
})
Или ещё точнее:
queryClient.invalidateQueries({
exact: true,
queryKey: ['users', userId]
})
Автоматический polling может создавать значительную нагрузку.
Пример:
useQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications,
refetchInterval: 1000
})
Запрос выполняется каждую секунду.
При большом количестве пользователей это приводит к:
Оптимизации:
refetchInterval: 30000
Или:
refetchInterval: data =>
data?.hasUpdates ? 5000 : 60000
Иногда проблема не в количестве запросов, а в размере ответа.
Пример неэффективного API:
{
"users": [
{
"id": 1,
"profile": {
"photos": [...],
"history": [...],
"settings": {...},
"statistics": {...}
}
}
]
}
Если компоненту нужен только name, загрузка мегабайт
данных становится узким местом.
Решения:
Chrome DevTools позволяют анализировать:
Критически важные показатели:
| Метрика | Описание |
|---|---|
| TTFB | Скорость ответа сервера |
| Content Download | Скорость передачи данных |
| Request Blocking | Очередь запросов |
| Waterfall | Последовательность выполнения |
Если TTFB высокий — проблема на сервере.
Если download длительный — проблема в payload.
Если blocking большой — превышен лимит параллельных соединений.
Даже быстрый запрос может вызывать медленный UI.
Пример:
const query = useQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
return (
<>
{query.data.map(user => (
<HeavyComponent key={user.id} user={user} />
))}
</>
)
Если список большой:
Оптимизации:
React.memo;Пример:
const HeavyComponent = React.memo(({ user }) => {
return <div>{user.name}</div>
})
Иногда задержка находится внутри queryFn.
Проблемный пример:
const fetchData = async () => {
const response = await fetch('/api/data')
const data = await response.json()
return expensiveTransform(data)
}
Если expensiveTransform выполняется сотни миллисекунд,
UI блокируется.
Диагностика:
console.time('transform')
const result = expensiveTransform(data)
console.timeEnd('transform')
Оптимизации:
Слишком маленький cacheTime приводит к постоянной
очистке кеша.
Пример:
useQuery({
queryKey: ['users'],
queryFn: fetchUsers,
cacheTime: 1000
})
Через секунду после потери observer кеш удаляется.
Следствия:
Обычно используют:
cacheTime: 1000 * 60 * 5
TanStack Query позволяет подписываться на события кеша.
Пример:
queryClient.getQueryCache().subscribe(event => {
console.log(event)
})
Полезно для анализа:
Такой подход помогает находить:
При SSR проблемы часто проявляются сильнее.
Типичные ошибки:
Корректная схема:
Сервер:
await queryClient.prefetchQuery({
queryKey: ['users'],
queryFn: fetchUsers
})
Клиент:
<Hydrate state={dehydratedState}>
<App />
</Hydrate>
Без hydration запрос может выполняться повторно после загрузки страницы.
Для полноценной диагностики производительности отслеживаются:
| Метрика | Значение |
|---|---|
| Request Duration | Время выполнения |
| Cache Hit Rate | Процент попаданий в кеш |
| Refetch Frequency | Частота обновлений |
| Retry Count | Количество повторов |
| Payload Size | Размер ответа |
| Observer Count | Количество подписчиков |
| Render Duration | Время рендера |
| Query Count | Число активных запросов |
| Concurrent Requests | Параллельные запросы |
| Memory Usage | Использование памяти |
Комплексный анализ этих показателей позволяет выявлять: