Диагностика медленных запросов

Медленные запросы в приложениях с TanStack Query могут появляться по множеству причин. Проблема не всегда связана непосредственно с сервером или сетью. Значительная часть задержек возникает из-за неправильной архитектуры клиентского кеша, неудачной конфигурации запросов, чрезмерного количества перерисовок компонентов или ошибок в механизмах синхронизации данных.

Основные источники проблем:

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

Диагностика должна охватывать одновременно:

  1. Сетевой уровень.
  2. Поведение TanStack Query.
  3. Производительность React-компонентов.
  4. Логику обновления кеша.
  5. Поведение API.

Использование Devtools для анализа производительности

Пакет Devtools предоставляет встроенные инструменты анализа запросов.

Установка:

npm install @tanstack/react-query-devtools

Подключение:

import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

function App() {
  return (
    <>
      <Application />
      <ReactQueryDevtools initialIsOpen={false} />
    </>
  )
}

Devtools позволяют анализировать:

  • количество запросов;
  • время жизни кеша;
  • состояние stale/fresh;
  • частоту refetch;
  • дублирование запросов;
  • повторные retries;
  • время обновления данных;
  • активные observers;
  • статус fetching.

Особенно полезен просмотр:

  • Last Updated
  • Observers
  • Data Explorer
  • Query 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
})

Такой подход позволяет:

  • собирать метрики;
  • отправлять данные в мониторинг;
  • выявлять деградацию API;
  • находить нестабильные endpoints.

Анализ waterfall-запросов

Одной из самых распространённых проблем являются 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 особенно опасны:

  • в мобильных сетях;
  • при SSR;
  • в dashboards;
  • в сложных административных панелях.

Диагностика повторных запросов

TanStack Query автоматически переиспользует кеш, однако ошибки конфигурации могут приводить к бесконечным refetch.

Типичный пример:

useQuery({
  queryKey: ['profile'],
  queryFn: fetchProfile,
  staleTime: 0
})

При staleTime: 0 данные моментально считаются устаревшими.

Следствия:

  • refetch при каждом mount;
  • refetch при фокусе окна;
  • refetch при reconnect;
  • лишняя нагрузка на сервер.

Более безопасная конфигурация:

useQuery({
  queryKey: ['profile'],
  queryFn: fetchProfile,
  staleTime: 1000 * 60 * 5
})

Дополнительные параметры:

useQuery({
  queryKey: ['profile'],
  queryFn: fetchProfile,
  refetchOnWindowFocus: false,
  refetchOnReconnect: false,
  refetchOnMount: false
})

Проверка количества observers

Каждый useQuery создаёт observer.

Если десятки компонентов подписаны на один и тот же запрос, возможны:

  • лишние обновления;
  • каскадные рендеры;
  • нагрузка на reconciliation React.

Пример проблемной архитектуры:

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} />
    </>
  )
}

Диагностика тяжёлых sel ect-функций

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}`
      }))
  }
})

Если массив содержит тысячи элементов, вычисления становятся дорогими.

Решения:

  • memoization;
  • серверная агрегация;
  • pagination;
  • virtualization;
  • вынос вычислений.

Использование useMemo:

const query = useQuery({
  queryKey: ['products'],
  queryFn: fetchProducts
})

const products = useMemo(() => {
  return query.data?.filter(p => p.active)
}, [query.data])

Выявление нестабильных queryKey

Нестабильные ключи — одна из наиболее скрытых причин деградации производительности.

Проблема:

useQuery({
  queryKey: ['search', filters],
  queryFn: fetchSearch
})

Если filters создаётся заново:

const filters = {
  category,
  sort
}

то TanStack Query может воспринимать ключ как новый.

Решение:

const filters = useMemo(() => ({
  category,
  sort
}), [category, sort])

Или:

queryKey: ['search', category, sort]

Анализ retries

Повторные попытки могут существенно замедлять интерфейс.

Стандартное поведение:

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

Мониторинг состояния fetching

Глобальное состояние загрузки помогает выявлять скрытые запросы.

Пример:

import { useIsFetching } fr om '@tanstack/react-query'

function GlobalLoader() {
  const isFetching = useIsFetching()

  return (
    <div>
      Active requests: {isFetching}
    </div>
  )
}

Если значение постоянно больше нуля, возможны:

  • циклические refetch;
  • polling;
  • бесконечные invalidateQueries;
  • ошибки enabled-условий.

Диагностика invalidateQueries

Неконтролируемая инвалидизация кеша часто становится причиной перегрузки API.

Плохой пример:

queryClient.invalidateQueries()

Такой вызов инвалидирует весь кеш приложения.

Следствия:

  • массовый refetch;
  • скачки CPU;
  • нагрузка на сеть;
  • блокировка интерфейса.

Предпочтительнее:

queryClient.invalidateQueries({
  queryKey: ['users']
})

Или ещё точнее:

queryClient.invalidateQueries({
  exact: true,
  queryKey: ['users', userId]
})

Анализ polling

Автоматический polling может создавать значительную нагрузку.

Пример:

useQuery({
  queryKey: ['notifications'],
  queryFn: fetchNotifications,
  refetchInterval: 1000
})

Запрос выполняется каждую секунду.

При большом количестве пользователей это приводит к:

  • перегрузке сервера;
  • повышенному расходу батареи;
  • нагрузке на CPU;
  • постоянным перерисовкам.

Оптимизации:

refetchInterval: 30000

Или:

refetchInterval: data =>
  data?.hasUpdates ? 5000 : 60000

Диагностика больших payload

Иногда проблема не в количестве запросов, а в размере ответа.

Пример неэффективного API:

{
  "users": [
    {
      "id": 1,
      "profile": {
        "photos": [...],
        "history": [...],
        "settings": {...},
        "statistics": {...}
      }
    }
  ]
}

Если компоненту нужен только name, загрузка мегабайт данных становится узким местом.

Решения:

  • DTO;
  • projection;
  • pagination;
  • lazy loading;
  • cursor pagination;
  • GraphQL selection;
  • server filtering.

Использование Network Tab

Chrome DevTools позволяют анализировать:

  • TTFB;
  • DNS lookup;
  • SSL handshake;
  • payload size;
  • compression;
  • waterfall;
  • blocking time.

Критически важные показатели:

Метрика Описание
TTFB Скорость ответа сервера
Content Download Скорость передачи данных
Request Blocking Очередь запросов
Waterfall Последовательность выполнения

Если TTFB высокий — проблема на сервере.

Если download длительный — проблема в payload.

Если blocking большой — превышен лимит параллельных соединений.


Диагностика React-перерисовок

Даже быстрый запрос может вызывать медленный UI.

Пример:

const query = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers
})
return (
  <>
    {query.data.map(user => (
      <HeavyComponent key={user.id} user={user} />
    ))}
  </>
)

Если список большой:

  • возникают массовые рендеры;
  • увеличивается commit time;
  • интерфейс начинает тормозить.

Оптимизации:

  • React.memo;
  • virtualization;
  • pagination;
  • windowing;
  • memoized props.

Пример:

const HeavyComponent = React.memo(({ user }) => {
  return <div>{user.name}</div>
})

Профилирование queryFn

Иногда задержка находится внутри 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')

Оптимизации:

  • Web Workers;
  • серверная обработка;
  • incremental processing;
  • streaming;
  • memoization.

Анализ cacheTime

Слишком маленький cacheTime приводит к постоянной очистке кеша.

Пример:

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  cacheTime: 1000
})

Через секунду после потери observer кеш удаляется.

Следствия:

  • постоянные повторные загрузки;
  • увеличение latency;
  • лишний сетевой трафик.

Обычно используют:

cacheTime: 1000 * 60 * 5

Использование логирования Query Cache

TanStack Query позволяет подписываться на события кеша.

Пример:

queryClient.getQueryCache().subscribe(event => {
  console.log(event)
})

Полезно для анализа:

  • создания query;
  • удаления query;
  • invalidation;
  • observer updates;
  • fetch start/end;
  • cache eviction.

Такой подход помогает находить:

  • бесконечные refetch;
  • дублирование ключей;
  • лишние invalidateQueries;
  • циклические обновления.

Диагностика SSR-запросов

При SSR проблемы часто проявляются сильнее.

Типичные ошибки:

  • двойной fetch;
  • hydration mismatch;
  • повторная загрузка после hydration;
  • отсутствие dehydrated state.

Корректная схема:

Сервер:

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 Использование памяти

Комплексный анализ этих показателей позволяет выявлять:

  • деградацию производительности;
  • ошибки архитектуры кеширования;
  • неэффективные queryKey;
  • избыточный refetch;
  • проблемы серверного API;
  • перегрузку React-компонентов;
  • сетевые узкие места.