React batching: как работает группировка обновлений

Разбираем batching в React на практике: очереди обновлений, автоматическая группировка в React 18+, flushSync, startTransition и production-ошибки.

06 марта 2026 г.18 минLexicon Team

Введение

React batching часто объясняют одной фразой: «React объединяет несколько setState». Для собеседования этого мало, а для реальной разработки - критично мало. Ошибки появляются в момент, когда команда не понимает границу между «обновление поставлено в очередь» и «обновление уже отражено в DOM».

Тема напрямую связана с тем, почему компонент перерисовывается и как React планирует работу. Для контекста полезно иметь под рукой разбор перерисовок в React, объяснение concurrent rendering и гид по оптимизации React-компонентов.

В статье разберем, как работает группировка обновлений, где batching действительно помогает, когда нужен flushSync, и какие ошибки чаще всего приводят к регрессиям в форме, поиске и сложных dashboard-экранах.

Больше вопросов в Telegram

Ежедневные разборы и реальные кейсы с интервью.

Подписаться

Что такое batching в React и зачем он нужен

Короткий ответ

Batching в React — это группировка нескольких обновлений состояния в один проход рендера. Цель простая: не тратить ресурсы на промежуточные состояния, которые пользователь все равно не увидит.

Что именно React группирует

React группирует обновления, которые попали в очередь до момента фактического выполнения работы рендера/коммита. Обычно это:

  • несколько вызовов setState в одном обработчике;
  • связка обновлений из промиса или таймера;
  • цепочка обновлений в одном «пакете» событий пользовательского взаимодействия.

Важно: batching не означает, что все обновления теряются в одно значение. Каждое обновление остается в очереди и применяется в нужном порядке. Группируется именно выполнение рендера, а не смысл бизнес-логики.

Почему это важно в production

Без batching каждое обновление запускало бы отдельный рендер. На интерактивных экранах это быстро превращается в лишнюю нагрузку:

  • выше CPU на слабых устройствах;
  • выше риск просадок FPS;
  • больше задержка между действием пользователя и стабильным интерфейсом.

Batching уменьшает технический шум, но не заменяет архитектуру состояния. Если состояние размазано по дереву без границ ответственности, группировка обновлений лишь замаскирует проблему.

Архитектура: место batching в жизненном цикле React

Чтобы понимать поведение, нужно разделять четыре сущности:

  1. Источник события: клик, ввод, ответ сети, таймер.
  2. Очередь обновлений: куда попадают setState.
  3. Планировщик React: какому обновлению дать приоритет и когда рендерить.
  4. Фаза commit: когда изменения реально попадают в DOM.

Упрощенный поток выглядит так:

  1. Событие добавляет несколько обновлений в очередь.
  2. React определяет, можно ли обработать их в одном цикле.
  3. Выполняется один рендер с учетом накопленных обновлений.
  4. Один commit применяет итог к DOM.

Границы ответственности:

  • batching: снижает число циклов рендера;
  • scheduler (планировщик): распределяет приоритеты и момент выполнения;
  • ваши компоненты: гарантируют корректные переходы состояния.

Типичная точка отказа: код «думает», что setState синхронно переписывает переменную и ее можно сразу прочитать в той же функции. В реальности чтение часто видит предыдущее значение, потому что commit еще не произошел.

Как batching ведет себя в реальном коде

Сценарий 1: несколько обновлений в одном обработчике

function CounterPair() {
  const [count, setCount] = useState(0);
  const [clicked, setClicked] = useState(false);

  const handleClick = () => {
    setCount((c) => c + 1);
    setCount((c) => c + 1);
    setClicked(true);
  };

  return (
    <section>
      <button onClick={handleClick}>+2</button>
      <p>count: {count}</p>
      <p>clicked: {String(clicked)}</p>
    </section>
  );
}

React не обязан делать три отдельных рендера. В типичном случае будет один цикл с финальным результатом: count + 2, clicked = true.

Критичный момент: используйте функциональные апдейтеры, если новое значение зависит от предыдущего. setCount(count + 1) дважды подряд часто приводит к неверному ожиданию результата.

Сценарий 2: асинхронный колбэк (таймер/промис)

В React 18+ автоматический batching распространяется и на многие асинхронные колбэки.

function SearchPanel() {
  const [query, setQuery] = useState("");
  const [items, setItems] = useState<string[]>([]);
  const [loading, setLoading] = useState(false);

  const runSearch = async (nextQuery: string) => {
    setLoading(true);

    const res = await fetch(`/api/search?q=${encodeURIComponent(nextQuery)}`);
    const data = await res.json();

    setItems(data.items ?? []);
    setLoading(false);
  };

  return (
    <div>
      <input
        value={query}
        onChange={(e) => {
          const next = e.target.value;
          setQuery(next);
          void runSearch(next);
        }}
      />
      {loading ? <p>Загрузка...</p> : <ul>{items.map((x) => <li key={x}>{x}</li>)}</ul>}
    </div>
  );
}

Если обновления setItems и setLoading(false) проходят в одном логическом участке, React обычно группирует их и снижает количество промежуточных перерисовок.

Сценарий 3: когда batching нужно прервать через flushSync

Иногда нужен немедленный commit: например, перед измерением layout после изменения флага видимости.

import { flushSync } from "react-dom";

function ExpandableMeasure() {
  const [open, setOpen] = useState(false);
  const boxRef = useRef<HTMLDivElement | null>(null);

  const openAndMeasure = () => {
    flushSync(() => {
      setOpen(true);
    });

    const height = boxRef.current?.getBoundingClientRect().height ?? 0;
    console.log("measured height:", height);
  };

  return (
    <section>
      <button onClick={openAndMeasure}>Открыть и измерить</button>
      {open && <div ref={boxRef}>Контент блока</div>}
    </section>
  );
}

flushSync полезен как скальпель, а не как архитектура. Частое применение ломает преимущества batching и делает интерфейс тяжелее.

Таблица сравнения: batching и связанные инструменты

КритерийAutomatic batchingflushSyncstartTransitionuseReducer
Главная задачаСнизить число рендеровФорсировать немедленный commitПонизить приоритет части обновленийЦентрализовать сложные переходы состояния
Влияет на приоритетыКосвенноНет, наоборот форсирует синхронностьДа, переводит часть работы в менее срочнуюНет напрямую
Риск неправильного примененияСреднийВысокийСреднийНизкий/средний
Типичный кейсНесколько setState в одном действииИзмерение DOM сразу после измененияФильтрация/поиск без блокировки вводаСложная форма/мастер с множеством экшенов
Что не решаетПлохую архитектуру и дублирование источников истиныПроизводительность в долгуюЛишние перерисовки от плохой композицииНизкоуровневый контроль commit-момента
Когда выбиратьПо умолчанию в React 18+Только точечно в горячем местеКогда важна отзывчивость вводаКогда бизнес-логика переходов стала неявной

Production pitfalls: где batching нарушает ожидаемое поведение

Ошибка 1. Читать state сразу после setState как «уже обновленный»

Симптомы:

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

Последствие: неочевидные race-сценарии и «фантомные» баги в формах.

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

Ошибка 2. Мешать срочные и несрочные обновления без приоритизации

Симптомы:

  • поле ввода подлагивает при тяжелой фильтрации;
  • ввод запаздывает на длинных списках;
  • profiling показывает длинные commit-фазы после каждого ввода.

Последствие: UX деградирует, хотя batching формально работает.

Профилактика: критичный UI-ответ оставлять срочным, тяжелую часть переносить в startTransition.

Ошибка 3. Массово использовать flushSync

Симптомы:

  • CPU растет на интерактивных сценариях;
  • команда постоянно «чинит» порядок апдейтов через форсированный commit;
  • локальная правка ломает соседние взаимодействия.

Последствие: приложение теряет преимущества планировщика React.

Профилактика: применять flushSync только там, где нужен синхронный DOM-read после DOM-write и нельзя переосмыслить поток.

Ошибка 4. Делать выводы о производительности без метрик

Симптомы:

  • «оптимизация batching» не меняет Web Vitals;
  • просадки остаются на реальных устройствах;
  • время уходит на тюнинг не того участка.

Последствие: команда оптимизирует рендеры, а узкое место в сети, сериализации или тяжелом вычислении.

Профилактика: сначала React Profiler и пользовательские метрики, потом точечные правки.

Разбор производительности

Batching снижает частоту рендеров, но не делает рендер бесплатным. На больших экранах стоимость одного commit все равно может быть высокой из-за:

  • тяжелых вычислений в render;
  • большого дерева с частыми изменениями props;
  • синхронных DOM-измерений и layout thrashing;
  • дорогих преобразований данных перед списками/графиками.

Когда batching действительно дает прирост:

  • действие пользователя вызывает 3-6 связанных обновлений;
  • в дереве много дочерних компонентов, чувствительных к ререндерам;
  • промежуточные состояния не несут ценности для UI.

Когда оптимизация преждевременна:

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

Практический подход:

  1. Снять профиль взаимодействия (ввод, клик, переключение).
  2. Зафиксировать commit duration и число рендеров.
  3. Внести изменение (например, startTransition или декомпозицию компонента).
  4. Повторно измерить и сравнить на том же сценарии.

Прокачай React за 7 дней

20 вопросов и разборов по React Hooks.

Начать

Когда batching не решает проблему

Есть три типовых класса проблем, где batching почти бесполезен:

  1. Плохие границы компонентов. Если один root-компонент тащит десятки поддеревьев, даже «сгруппированный» рендер останется тяжелым.
  2. Нестабильные ссылки и ненужные пересоздания функций/объектов. Без контроля мемоизации дочерние компоненты будут ререндериться слишком часто.
  3. Высокая цена вычислений. Если фильтрация списка на 30k элементов выполняется синхронно на каждом вводе, batching лишь немного сгладит симптом.

В таких случаях сначала исправляют архитектуру: делят дерево, локализуют состояние, убирают лишние зависимости, применяют мемоизацию по профилю.

Практики, которые работают в командах

  • Явно маркируйте тип обновлений: срочные (ввод, клики) и несрочные (дорогая постобработка).
  • В ревью спрашивайте: «может ли этот setState зависеть от предыдущего значения?» Если да, используйте функциональный апдейтер.
  • Для сложных потоков переходите на useReducer, чтобы очередь изменений была явной.
  • Наблюдаемость: логируйте длительность критичных интеракций и частоту медленных commit-фаз.
  • Тестирование: добавляйте сценарии быстрых последовательных действий пользователя, а не только единичные клики.
  • Rollout: включайте тяжелые оптимизации через feature flag, чтобы можно было быстро откатить спорное решение.

Частые ошибки

  • Ожидать, что setState немедленно изменит переменную в той же строке кода.
  • Использовать flushSync как универсальное средство «сделать правильно».
  • Не разделять срочные и несрочные обновления на экранах поиска и фильтрации.
  • Пытаться решить архитектурную перегрузку batching-магией.
  • Измерять только локально на мощной машине и игнорировать слабые устройства.

Как отвечать на интервью про React batching

Рабочий шаблон для junior/middle:

  1. Batching — это объединение нескольких обновлений в один цикл рендера/коммита.
  2. В React 18+ автоматическая группировка работает не только в обработчиках событий, но и в типичном async-коде.
  3. Если обновление зависит от предыдущего, используем функциональный апдейтер.
  4. flushSync применяем точечно, когда нужен немедленный DOM после обновления.
  5. Для отзывчивого UI вместе с batching часто используют startTransition.

Такой ответ показывает понимание не только API, но и того, как React планирует работу под нагрузкой.

Подготовься к React-собеседованию на реальных задачах

Разберем batching, ререндеры и приоритизацию обновлений в формате mock-интервью с обратной связью.

Начать подготовку

Практический кейс из production: поиск без лагов при вводе

Сценарий: в админке есть таблица на 10k строк. Пользователь печатает в поле поиска, и на каждый символ выполняется фильтрация и несколько связанных обновлений UI.

Плохой вариант:

  • все обновления считаются одинаково срочными;
  • фильтрация запускается в том же приоритете, что и ввод;
  • поле начинает «подвисать» на слабых ноутбуках.

Рабочий вариант:

import { startTransition, useMemo, useState } from "react";

type Row = { id: string; name: string; email: string };

function UsersTable({ rows }: { rows: Row[] }) {
  const [query, setQuery] = useState("");
  const [filter, setFilter] = useState("");

  const visibleRows = useMemo(() => {
    const q = filter.trim().toLowerCase();
    if (!q) return rows;
    return rows.filter((r) => {
      return (
        r.name.toLowerCase().includes(q) ||
        r.email.toLowerCase().includes(q)
      );
    });
  }, [rows, filter]);

  const onChange = (next: string) => {
    // Срочное обновление: пользователь должен видеть ввод без задержки.
    setQuery(next);

    // Несрочное обновление: тяжелая фильтрация может подождать.
    startTransition(() => {
      setFilter(next);
    });
  };

  return (
    <section>
      <input
        value={query}
        onChange={(e) => onChange(e.target.value)}
        placeholder="Поиск по имени или email"
      />
      <p>Найдено: {visibleRows.length}</p>
      <ul>
        {visibleRows.slice(0, 200).map((u) => (
          <li key={u.id}>{u.name} - {u.email}</li>
        ))}
      </ul>
    </section>
  );
}

Что здесь делает batching:

  • объединяет связанные обновления, снижая лишние ререндеры;
  • вместе с приоритизацией (startTransition) удерживает ввод отзывчивым.

Граница подхода: если список очень большой, одного batching недостаточно. Нужны виртуализация, серверный поиск или индексация данных.

Чек-лист внедрения batching-стратегии в существующий код

  1. Есть ли место, где подряд вызываются несколько setState? Если да, проверьте, нет ли лишних промежуточных состояний и неправильных чтений state.

  2. Зависит ли новое состояние от предыдущего? Если да, используйте функциональный апдейтер, иначе получите устаревшее значение.

  3. Есть ли тяжелая несрочная работа после ввода? Если да, рассмотрите startTransition.

  4. Реально ли нужен flushSync? Если цель только «починить порядок», обычно лучше переразложить поток состояния.

  5. Измерили ли вы эффект? Сравните профили до/после и подтвердите улучшение метриками.

Этот чек-лист хорошо работает на ревью: он отсеивает псевдооптимизации и оставляет только изменения с проверяемой пользой.

FAQ

Batching гарантирует один рендер на любое действие?

Нет. React старается группировать обновления, но итог зависит от приоритетов, границ задач и того, где именно происходят обновления.

Почему после setState я иногда вижу старое значение в логе?

Потому что обновление поставлено в очередь, а commit еще не завершен. Для вычисления от прошлого значения используйте функциональный апдейтер.

Нужно ли везде использовать startTransition вместе с batching?

Нет. startTransition нужен там, где есть конкуренция между срочным UI-ответом и тяжелой несрочной работой.

Может ли batching ухудшить дебаг?

Может усложнить ментальную модель, если код рассчитывает на синхронные побочные эффекты внутри обработчиков. Проблема не в batching, а в неявном потоке состояния.

Что чаще всего спрашивают на собеседовании по batching?

Разницу между «обновление запланировано» и «обновление применено», поведение в React 18+, назначение flushSync и связь batching со startTransition.

Итоги

Batching в React снижает лишнюю работу рендера и делает интерфейс стабильнее под нагрузкой, но только при корректной архитектуре состояния. Если смешивать приоритеты, читать state как синхронный и злоупотреблять flushSync, получаются те же лаги, только сложнее отлаживаемые.

Рабочая стратегия: держать обновления явными, разделять срочные и несрочные задачи, измерять эффект профилировщиком и применять низкоуровневые инструменты точечно.

Для расширения темы полезно пройти разбор reconciliation, материал про Context API и границы состояния и сравнение memo/useMemo/useCallback.

Больше вопросов в Telegram

Ежедневные разборы и реальные кейсы с интервью.

Подписаться

Автор

Lexicon Team

Читайте также