Batching запросов

Batching запросов в TanStack Query — это механизм группировки нескольких операций обновления состояния запросов в единый цикл рендера или единый пакет сетевых и UI-обновлений, позволяющий снизить количество лишних перерисовок, уменьшить нагрузку на React и ускорить обработку связанных запросов.

TanStack Query сам по себе не реализует сетевой batching (объединение HTTP-запросов в один), однако предоставляет и опирается на batching обновлений состояния через React и внутренний query cache. Это особенно важно в сценариях, где одновременно изменяется несколько query или происходит каскадное обновление данных после мутаций.


Внутри TanStack Query batching проявляется в двух формах:

  1. Batching обновлений состояния (state batching)
  2. Batching триггеров перерендеринга React

Основная цель — объединить несколько вызовов setState, invalidateQueries, setQueryData и подобных операций в один цикл обновления UI.

Ключевой эффект:

  • уменьшение количества render-циклов React
  • уменьшение количества промежуточных состояний
  • повышение предсказуемости обновлений cache

Как React влияет на batching

TanStack Query не изолирован от механизма React batching. В современных версиях React (18+) используется автоматический batching, который объединяет обновления, происходящие внутри:

  • промисов
  • событий
  • таймеров
  • async функций

Пример поведения:

queryClient.setQueryData(['user', 1], user);
queryClient.invalidateQueries({ queryKey: ['orders'] });
queryClient.setQueryData(['settings'], settings);

В React 18 эти три операции чаще всего приводят к одному render-циклу, а не к трём отдельным.


Внутренний batching в QueryCache

TanStack Query использует QueryCache как центральный механизм хранения и распространения изменений.

Каждое изменение:

  • обновление query state
  • инвалидирование кеша
  • установка новых данных
  • префетчинг

проходит через уведомления подписчиков.

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

Модель поведения

  1. Выполняется серия изменений в QueryCache
  2. Изменения помечаются как “dirty”
  3. В конце микротаска выполняется flush уведомлений
  4. Подписчики получают уже финальное состояние

Это предотвращает ситуацию, когда UI реагирует на каждое промежуточное состояние cache.


Бatching при invalidateQueries

Одна из самых частых причин множественных обновлений — инвалидирование нескольких ключей.

queryClient.invalidateQueries({ queryKey: ['user'] });
queryClient.invalidateQueries({ queryKey: ['posts'] });
queryClient.invalidateQueries({ queryKey: ['comments'] });

Без batching это могло бы вызвать:

  • три отдельных обновления cache
  • три пересчёта активных запросов
  • три потенциальных refetch-процесса

Внутренне TanStack Query старается объединять такие вызовы в один проход планировщика обновлений.


Сетевые эффекты batching через refetch

Важно различать batching обновлений и сетевой batching.

TanStack Query не объединяет HTTP-запросы автоматически, но batching влияет на то, когда эти запросы стартуют.

Пример:

queryClient.invalidateQueries({ queryKey: ['user'] });
queryClient.invalidateQueries({ queryKey: ['posts'] });

Вместо мгновенного запуска двух независимых рефетчей возможен сценарий:

  • оба query помечаются stale
  • scheduler запускает refetch в одном цикле
  • запросы уходят почти одновременно

Это даёт эффект “псевдо-batching” на уровне сети.


Batching внутри React-обновлений подписчиков

Каждый useQuery подписывается на изменения конкретного query.

const { data: user } = useQuery({
  queryKey: ['user', id],
  queryFn: fetchUser
});

const { data: posts } = useQuery({
  queryKey: ['posts', id],
  queryFn: fetchPosts
});

Если одновременно происходят изменения:

  • user обновился
  • posts инвалидирован

без batching это привело бы к двум render-циклам компонента.

С batching:

  • оба обновления объединяются
  • компонент перерендеривается один раз

microtask batching и scheduling

TanStack Query использует планировщик, основанный на microtask queue.

Общий принцип:

  1. Изменения добавляются в очередь
  2. Microtask (Promise.resolve) планирует flush
  3. В конце текущего JS-стека выполняется пакетное обновление

Это критично для следующих сценариев:

  • последовательные invalidateQueries
  • массовые setQueryData после мутации
  • цепочки dependent queries

Batching при мутациях (mutations)

Мутации часто вызывают серию связанных обновлений cache:

mutation.mutate(data, {
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['list'] });
    queryClient.invalidateQueries({ queryKey: ['stats'] });
    queryClient.setQueryData(['lastUpdate'], Date.now());
  }
});

Без batching:

  • три независимых обновления cache
  • три потенциальных refetch
  • три UI update

С batching:

  • все изменения объединяются в один flush
  • React получает один согласованный snapshot состояния

setQueryData и batching

setQueryData — одна из самых “частых” операций, приводящих к множественным обновлениям.

queryClient.setQueryData(['cart'], old => [...old, item]);
queryClient.setQueryData(['cartCount'], c => c + 1);

TanStack Query группирует такие изменения, если они происходят в одном синхронном контексте, минимизируя количество уведомлений подписчиков.


Границы batching: когда он не работает

Batching имеет ограничения и не гарантируется в следующих случаях:

1. Разные асинхронные тики

queryClient.setQueryData(['a'], 1);

setTimeout(() => {
  queryClient.setQueryData(['b'], 2);
}, 0);

Здесь batching не объединяет операции, так как они происходят в разных event loop циклах.


2. Принудительные flush-операции

Некоторые внутренние операции могут вызывать немедленное уведомление подписчиков, например:

  • критические обновления cache
  • некоторые optimistic update сценарии

3. Внешние side effects

Если обновление вызывает внешние события (например, websocket sync), batching может быть частично обойдён ради консистентности данных.


Оптимизация batching в архитектуре приложения

Эффективное использование batching достигается за счёт:

Группировки логически связанных операций

queryClient.setQueryData(['user', id], user);
queryClient.setQueryData(['profile', id], profile);
queryClient.invalidateQueries({ queryKey: ['dashboard'] });

вместо разнесения по разным async шагам.


Минимизации промежуточных state updates

Частые последовательные setQueryData без необходимости приводят к:

  • лишним cache notifications
  • избыточным render cycles

Использования transactional подхода

Логика мутаций может быть структурирована как единый блок:

  • обновление cache
  • инвалидирование зависимостей
  • установка optimistic state

в одном синхронном контексте, чтобы batching сработал максимально эффективно.


Взаимодействие batching с devtools

TanStack Query Devtools визуализируют каждое изменение cache, что иногда создаёт иллюзию отсутствия batching.

На самом деле:

  • Devtools могут показывать промежуточные состояния
  • UI при этом получает уже сгруппированный результат

Это различие важно при профилировании производительности.


Практическое влияние batching на производительность

Batching влияет на три уровня:

  1. React rendering

    • меньше render cycles
    • меньше reconciliation операций
  2. Query processing

    • меньше перерасчётов зависимости queries
    • оптимизация stale-state propagation
  3. Network scheduling

    • группировка refetch запросов в один временной интервал

Типичные ошибки при работе с batching

Разрыв синхронного контекста

Разделение операций на разные microtasks без необходимости приводит к потере batching.


Избыточные invalidateQueries

Частое инвалидирование больших групп ключей вызывает:

  • cascade refetch
  • перегрузку сети
  • лишние rerender циклы

Неправильное ожидание “сетевого batching”

Ожидание, что TanStack Query объединит HTTP-запросы, приводит к архитектурным ошибкам. Библиотека не выполняет request deduplication на уровне payload.


Итоговая модель поведения batching

Внутренне поведение можно представить как конвейер:

  1. Операции mutate cache
  2. Пометка изменений
  3. Планирование flush через microtask
  4. Единый batch обновлений
  5. React render с консистентным snapshot
  6. При необходимости — запуск сгруппированных refetch

Эта модель обеспечивает баланс между реактивностью и производительностью, минимизируя лишние вычисления и сохраняя предсказуемость состояния приложения.