Сбор метрик производительности

Сбор метрик производительности позволяет анализировать поведение клиентского кеша, скорость выполнения запросов, частоту повторных обращений к серверу, объём сетевой активности и реакцию интерфейса на изменения состояния данных. В крупных приложениях TanStack Query становится центральным механизмом взаимодействия с API, поэтому деградация производительности напрямую влияет на UX, нагрузку на backend и потребление памяти.

Метрики необходимы для решения нескольких задач:

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

Какие показатели обычно собираются

Время выполнения запроса

Наиболее базовая метрика — длительность выполнения queryFn.

const start = performance.now()

const data = await fetch('/api/users')

const end = performance.now()

console.log(`Запрос занял ${end - start}ms`)

В TanStack Query подобные измерения обычно внедряются централизованно внутри HTTP-клиента.


Количество запросов

Позволяет определить:

  • слишком частые refetch;
  • циклические обновления;
  • лишние invalidate;
  • ошибочные зависимости.

Пример счётчика:

let requestCounter = 0

async function api(url) {
    requestCounter++

    return fetch(url)
}

Частота cache hit

Одна из важнейших метрик.

Высокий показатель cache hit означает:

  • снижение нагрузки на API;
  • уменьшение времени загрузки;
  • более стабильный UI.

Низкий показатель указывает на:

  • неправильные queryKey;
  • слишком маленький staleTime;
  • агрессивный refetch;
  • постоянный invalidateQueries.

Время до появления данных

Анализируется:

  • время первого ответа;
  • время появления stale-данных;
  • скорость hydration;
  • длительность background refetch.

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

Позволяет обнаружить:

  • лавинообразные refetch;
  • параллельные дубликаты;
  • ошибки в infinite query;
  • бесконтрольные prefetch-операции.

Использование Query Cache для сбора метрик

TanStack Query предоставляет доступ к внутреннему кешу через QueryClient.

const queryClient = new QueryClient()

Через queryCache можно подписываться на изменения состояния запросов.


Подписка на Query Cache

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

События содержат:

  • тип изменения;
  • объект query;
  • текущее состояние;
  • метаданные.

Анализ query state

Пример анализа состояния:

queryClient
    .getQueryCache()
    .subscribe((event) => {
        const query = event.query

        console.log({
            key: query.queryKey,
            status: query.state.status,
            fetchStatus: query.state.fetchStatus,
            dataUpdatedAt: query.state.dataUpdatedAt,
            errorUpdatedAt: query.state.errorUpdatedAt
        })
    })

Измерение времени выполнения запросов

Централизованный HTTP-клиент

Наиболее правильный подход — измерение внутри API-слоя.

async function request(url, options) {
    const start = performance.now()

    const response = await fetch(url, options)

    const end = performance.now()

    console.log({
        url,
        duration: end - start
    })

    return response.json()
}

Интеграция с queryFn

useQuery({
    queryKey: ['users'],
    queryFn: () => request('/api/users')
})

Сбор статистики retry

Retry могут существенно увеличивать нагрузку.

useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    retry: 3
})

Для анализа retry полезно фиксировать:

  • количество повторов;
  • итоговую успешность;
  • длительность между retry;
  • тип ошибки.

Перехват ошибок

const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            onError(error) {
                console.log(error)
            }
        }
    }
})

Анализ количества ререндеров

Чрезмерные ререндеры — распространённая проблема.

Причины:

  • нестабильные queryKey;
  • select-функции с новыми ссылками;
  • постоянные invalidate;
  • refetchOnWindowFocus;
  • ошибки memoization.

React Profiler

Для анализа ререндеров используется React Profiler.

<Profiler
    id="UsersPage"
    onRen der={(id, phase, duration) => {
        console.log({
            id,
            phase,
            duration
        })
    }}
>
    <UsersPage />
</Profiler>

Измерение времени background refetch

TanStack Query выполняет фоновые обновления автоматически.

Важно измерять:

  • длительность refetch;
  • частоту запуска;
  • влияние на UI;
  • объём сетевых запросов.

Подписка на fetch status

queryClient
    .getQueryCache()
    .subscribe((event) => {
        const state = event.query.state

        console.log(state.fetchStatus)
    })

Возможные значения:

  • idle
  • fetching
  • paused

Анализ staleTime

Неправильно подобранный staleTime приводит либо к устаревшим данным, либо к чрезмерному количеству запросов.

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

При сборе метрик оценивают:

  • среднее количество refetch;
  • частоту cache miss;
  • задержки интерфейса;
  • нагрузку на backend.

Анализ gcTime

gcTime влияет на объём памяти.

useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
    gcTime: 1000 * 60 * 30
})

Слишком большое значение приводит к:

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

Сбор метрик кеша

Количество query

const queries = queryClient
    .getQueryCache()
    .findAll()

console.log(queries.length)

Размер кеша

TanStack Query не предоставляет размер памяти напрямую, но можно вычислять приблизительный объём.

const size = JSON.stringify(
    queryClient.getQueryData(['users'])
).length

Анализ активности invalidateQueries

Чрезмерный invalidate — частая причина проблем производительности.

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

Полезно логировать:

  • кто вызвал invalidate;
  • как часто вызывается;
  • сколько query затронуто;
  • привёл ли invalidate к refetch.

Инструментирование invalidateQueries

const originalInvalidate =
    queryClient.invalidateQueries.bind(queryClient)

queryClient.invalidateQueries = async (...args) => {
    console.log('invalidateQueries', args)

    return originalInvalidate(...args)
}

Метрики Infinite Query

Infinite Query требуют отдельного мониторинга.

Проблемы:

  • дублирование страниц;
  • повторная загрузка;
  • накопление памяти;
  • лавинообразный refetch.

Анализ количества страниц

const data = queryClient.getQueryData(['feed'])

console.log(data.pages.length)

Мониторинг prefetch

Prefetch может незаметно генерировать большое количество запросов.

queryClient.prefetchQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
})

Важно измерять:

  • процент реально использованных prefetch;
  • объём бесполезного трафика;
  • время жизни prefetched-данных.

Использование Performance API

Browser Performance API подходит для глубокой диагностики.


Performance Mark

performance.mark('users-fetch-start')

await fetch('/api/users')

performance.mark('users-fetch-end')

Performance Measure

performance.measure(
    'users-fetch',
    'users-fetch-start',
    'users-fetch-end'
)

Получение результатов

const entries =
    performance.getEntriesByName('users-fetch')

console.log(entries)

Интеграция с monitoring-системами

Метрики часто отправляются в:

  • Grafana;
  • Prometheus;
  • Datadog;
  • New Relic;
  • Sentry;
  • OpenTelemetry.

Интеграция с Sentry

Логирование ошибок query

const queryClient = new QueryClient({
    defaultOptions: {
        queries: {
            onError(error) {
                Sentry.captureException(error)
            }
        }
    }
})

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

async function request(url) {
    const start = performance.now()

    const response = await fetch(url)

    const duration =
        performance.now() - start

    if (duration > 2000) {
        Sentry.captureMessage(
            `Slow request: ${url}`
        )
    }

    return response.json()
}

Интеграция с OpenTelemetry

OpenTelemetry позволяет строить полноценный distributed tracing.

Обычно собираются:

  • длительность запросов;
  • цепочки зависимостей;
  • количество retry;
  • сетевые ошибки;
  • latency API.

Создание trace

const tracer = trace.getTracer('frontend')

const span = tracer.startSpan('users-query')

Завершение trace

span.end()

Метрики hydration

При SSR и hydration важно анализировать:

  • размер dehydrated state;
  • время hydration;
  • повторные запросы после hydration;
  • рассинхронизацию кеша.

Измерение hydration

const start = performance.now()

hydrate(queryClient, dehydratedState)

const end = performance.now()

console.log(end - start)

Мониторинг Devtools

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

  • состояние query;
  • время обновлений;
  • cache lifetime;
  • background fetching;
  • retry;
  • observers;
  • garbage collection.

Кастомный сборщик метрик

Крупные приложения часто используют собственный metrics layer.


Пример metrics collector

class QueryMetrics {
    requests = 0
    errors = 0
    cacheHits = 0
    cacheMisses = 0

    logRequest() {
        this.requests++
    }

    logError() {
        this.errors++
    }

    logCacheHit() {
        this.cacheHits++
    }

    logCacheMiss() {
        this.cacheMisses++
    }

    getStats() {
        return {
            requests: this.requests,
            errors: this.errors,
            cacheHits: this.cacheHits,
            cacheMisses: this.cacheMisses
        }
    }
}

Интеграция metrics collector

const metrics = new QueryMetrics()

async function request(url) {
    metrics.logRequest()

    try {
        const response = await fetch(url)

        return response.json()
    } catch (error) {
        metrics.logError()

        throw error
    }
}

Сбор пользовательских метрик

Иногда требуется бизнес-аналитика.

Например:

  • сколько времени пользователь ждал данные;
  • сколько запросов выполнил экран;
  • сколько раз обновлялся кеш;
  • сколько retry произошло до успешной загрузки.

Метрики UI responsiveness

Даже быстрые запросы могут вызывать лаги интерфейса.

Причины:

  • тяжёлые select;
  • большие JSON;
  • массовые invalidate;
  • синхронные преобразования данных.

Анализ select-функций

useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
    select(data) {
        const start = performance.now()

        const result = heavyTransform(data)

        const end = performance.now()

        console.log(end - start)

        return result
    }
})

Метрики сетевой деградации

Полезно отслеживать:

  • offline/online переходы;
  • paused queries;
  • время восстановления;
  • количество failed requests.

Анализ network mode

useQuery({
    queryKey: ['posts'],
    queryFn: fetchPosts,
    networkMode: 'offlineFirst'
})

Мониторинг observer count

Большое количество observers увеличивает нагрузку.

queryClient
    .getQueryCache()
    .findAll()
    .forEach((query) => {
        console.log(
            query.getObserversCount()
        )
    })

Выявление дублирующих запросов

Иногда несколько компонентов инициируют одинаковые query с разными ключами.

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

['user', 1]
['users', 1]
['profile', 1]

Фактически это могут быть одинаковые данные, но TanStack Query создаст разные кеш-записи.


Метрики сериализации

Большие объёмы данных могут тормозить:

  • hydration;
  • persistence;
  • broadcastQueryClient;
  • localStorage persistence.

Измерение сериализации

const start = performance.now()

JSON.stringify(data)

const end = performance.now()

console.log(end - start)

Сбор Web Vitals

TanStack Query влияет на:

  • LCP;
  • INP;
  • CLS;
  • TTFB.

Особенно заметно это при:

  • waterfall-запросах;
  • долгих refetch;
  • блокирующих select;
  • excessive suspense.

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

Плохая архитектура:

const user = useQuery(...)
const posts = useQuery(...)
const comments = useQuery(...)

Если запросы зависят друг от друга, возникают последовательные задержки.


Оптимизация мониторинга

Сбор метрик сам по себе не должен создавать нагрузку.

Обычно применяются:

  • sampling;
  • debounce;
  • batching;
  • async logging;
  • ограничение payload;
  • агрегация на клиенте.

Sampling метрик

function shouldSample(rate = 0.1) {
    return Math.random() < rate
}

Batch-отправка метрик

const queue = []

function pushMetric(metric) {
    queue.push(metric)
}

Периодическая отправка

setInterval(() => {
    if (!queue.length) {
        return
    }

    sendMetrics(queue)

    queue.length = 0
}, 5000)

Частые проблемы при сборе метрик

Избыточное логирование

Может приводить к:

  • утечкам памяти;
  • лагам интерфейса;
  • переполнению monitoring-систем.

Синхронная обработка

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

onSuccess(data) {
    heavyAnalytics(data)
}

Тяжёлая аналитика блокирует главный поток.


Логирование больших payload

JSON размером в несколько мегабайт способен существенно замедлить приложение.


Отсутствие агрегации

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


Практическая архитектура production-мониторинга

Обычно используется следующая схема:

  1. HTTP client собирает latency.
  2. Query Cache отслеживает состояние query.
  3. Error layer собирает исключения.
  4. Performance API измеряет UI.
  5. Metrics aggregator объединяет события.
  6. Batch sender отправляет данные на backend.
  7. Monitoring platform визуализирует статистику.

Пример комплексного мониторинга

const metrics = {
    requests: 0,
    errors: 0,
    slowRequests: 0
}

async function api(url) {
    metrics.requests++

    const start = performance.now()

    try {
        const response = await fetch(url)

        return await response.json()
    } catch (error) {
        metrics.errors++

        throw error
    } finally {
        const duration =
            performance.now() - start

        if (duration > 1000) {
            metrics.slowRequests++
        }
    }
}