React batching: как работает группировка обновлений
Разбираем batching в React на практике: очереди обновлений, автоматическая группировка в React 18+, flushSync, startTransition и production-ошибки.
- Введение
- Что такое batching в React и зачем он нужен
- Короткий ответ
- Что именно React группирует
- Почему это важно в production
- Архитектура: место batching в жизненном цикле React
- Как batching ведет себя в реальном коде
- Сценарий 1: несколько обновлений в одном обработчике
- Сценарий 2: асинхронный колбэк (таймер/промис)
- Сценарий 3: когда batching нужно прервать через flushSync
- Таблица сравнения: batching и связанные инструменты
- Production pitfalls: где batching нарушает ожидаемое поведение
- Ошибка 1. Читать state сразу после setState как «уже обновленный»
- Ошибка 2. Мешать срочные и несрочные обновления без приоритизации
- Ошибка 3. Массово использовать flushSync
- Ошибка 4. Делать выводы о производительности без метрик
- Разбор производительности
- Когда batching не решает проблему
- Практики, которые работают в командах
- Частые ошибки
- Как отвечать на интервью про React batching
- Практический кейс из production: поиск без лагов при вводе
- Чек-лист внедрения batching-стратегии в существующий код
- FAQ
- Batching гарантирует один рендер на любое действие?
- Почему после setState я иногда вижу старое значение в логе?
- Нужно ли везде использовать startTransition вместе с batching?
- Может ли batching ухудшить дебаг?
- Что чаще всего спрашивают на собеседовании по batching?
- Итоги
Введение
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
Чтобы понимать поведение, нужно разделять четыре сущности:
- Источник события: клик, ввод, ответ сети, таймер.
- Очередь обновлений: куда попадают
setState. - Планировщик React: какому обновлению дать приоритет и когда рендерить.
- Фаза commit: когда изменения реально попадают в DOM.
Упрощенный поток выглядит так:
- Событие добавляет несколько обновлений в очередь.
- React определяет, можно ли обработать их в одном цикле.
- Выполняется один рендер с учетом накопленных обновлений.
- Один 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 batching | flushSync | startTransition | useReducer |
|---|---|---|---|---|
| Главная задача | Снизить число рендеров | Форсировать немедленный 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, а тяжелой бизнес-функцией;
- рендер уже быстрый, но кеш/сеть медленные.
Практический подход:
- Снять профиль взаимодействия (ввод, клик, переключение).
- Зафиксировать commit duration и число рендеров.
- Внести изменение (например,
startTransitionили декомпозицию компонента). - Повторно измерить и сравнить на том же сценарии.
Прокачай React за 7 дней
20 вопросов и разборов по React Hooks.
Когда batching не решает проблему
Есть три типовых класса проблем, где batching почти бесполезен:
- Плохие границы компонентов. Если один root-компонент тащит десятки поддеревьев, даже «сгруппированный» рендер останется тяжелым.
- Нестабильные ссылки и ненужные пересоздания функций/объектов. Без контроля мемоизации дочерние компоненты будут ререндериться слишком часто.
- Высокая цена вычислений. Если фильтрация списка на 30k элементов выполняется синхронно на каждом вводе, batching лишь немного сгладит симптом.
В таких случаях сначала исправляют архитектуру: делят дерево, локализуют состояние, убирают лишние зависимости, применяют мемоизацию по профилю.
Практики, которые работают в командах
- Явно маркируйте тип обновлений: срочные (ввод, клики) и несрочные (дорогая постобработка).
- В ревью спрашивайте: «может ли этот
setStateзависеть от предыдущего значения?» Если да, используйте функциональный апдейтер. - Для сложных потоков переходите на
useReducer, чтобы очередь изменений была явной. - Наблюдаемость: логируйте длительность критичных интеракций и частоту медленных commit-фаз.
- Тестирование: добавляйте сценарии быстрых последовательных действий пользователя, а не только единичные клики.
- Rollout: включайте тяжелые оптимизации через feature flag, чтобы можно было быстро откатить спорное решение.
Частые ошибки
- Ожидать, что
setStateнемедленно изменит переменную в той же строке кода. - Использовать
flushSyncкак универсальное средство «сделать правильно». - Не разделять срочные и несрочные обновления на экранах поиска и фильтрации.
- Пытаться решить архитектурную перегрузку batching-магией.
- Измерять только локально на мощной машине и игнорировать слабые устройства.
Как отвечать на интервью про React batching
Рабочий шаблон для junior/middle:
- Batching — это объединение нескольких обновлений в один цикл рендера/коммита.
- В React 18+ автоматическая группировка работает не только в обработчиках событий, но и в типичном async-коде.
- Если обновление зависит от предыдущего, используем функциональный апдейтер.
flushSyncприменяем точечно, когда нужен немедленный DOM после обновления.- Для отзывчивого 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-стратегии в существующий код
-
Есть ли место, где подряд вызываются несколько
setState? Если да, проверьте, нет ли лишних промежуточных состояний и неправильных чтений state. -
Зависит ли новое состояние от предыдущего? Если да, используйте функциональный апдейтер, иначе получите устаревшее значение.
-
Есть ли тяжелая несрочная работа после ввода? Если да, рассмотрите
startTransition. -
Реально ли нужен
flushSync? Если цель только «починить порядок», обычно лучше переразложить поток состояния. -
Измерили ли вы эффект? Сравните профили до/после и подтвердите улучшение метриками.
Этот чек-лист хорошо работает на ревью: он отсеивает псевдооптимизации и оставляет только изменения с проверяемой пользой.
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
Читайте также
frontend
React и TypeScript: частые вопросы на интервью
Разбираем частые вопросы на интервью по React и TypeScript: типизация props, hooks, events, generics, refs, discriminated unions. А также типичные ошибки кандидатов и примеры сильных ответов.
frontend
useRef: как работает и где используют на практике
Разбираем useRef в React без мифов: внутренняя механика, DOM-кейсы, production-ошибки, производительность и ответы для собеседования.
frontend
React Strict Mode: зачем он нужен
Подробно разбираем React Strict Mode: какие проверки он включает, почему в dev все «вызывается дважды», какие баги ловит и как безопасно внедрять в production-командах.