Event Loop и React: как связаны и почему это влияет на рендер
Разбираем связь Event Loop и React без упрощений: macrotask, microtask, batching, commit, useEffect, scheduler, производительность и типичные ошибки на интервью.
- Введение
- Что такое Event Loop в контексте React
- Архитектурный разбор: где встречаются Event Loop, Scheduler и React Fiber
- Контекст задачи
- Схема компонентов и ролей
- Поток события
- Узкие места и деградация
- Microtask, macrotask и React: где разница становится заметной
- Сравнение подходов: где именно планировать работу
- Разбор производительности: почему Event Loop напрямую влияет на UX React-приложения
- Production pitfalls: где связь Event Loop и React ломают чаще всего
- 1. Лечат лаги через setTimeout, не понимая источник блокировки
- 2. Делают тяжелую работу в useEffect, считая, что это бесплатно
- 3. Путают batching с отложенным выполнением
- 4. Игнорируют стоимость пользовательского JavaScript вне React
- Практики, которые обычно работают лучше всего
- Архитектурные практики
- Практики кода
- Наблюдаемость и тестирование
- Rollout и откат
- Частые ошибки
- Как отвечать на интервью
- FAQ
- Нужно ли фронтенд-разработчику глубоко знать Event Loop?
- Почему после setState лог иногда показывает старое значение?
- Чем microtask опаснее, чем кажется?
- Почему useEffect не подходит для любой логики?
- React concurrent rendering делает код параллельным?
- Итоги
Введение
Тема Event Loop и React кажется базовой только до первого сложного бага. Пока приложение простое, хватает фразы «React обновляет состояние асинхронно». Но как только в интерфейсе появляются лаги при вводе, каскадные ререндеры, двойные эффекты, проблемы с Promise и странный порядок логов, без модели Event Loop картина быстро разваливается.
Связь здесь прямая. React не исполняется в вакууме. Он живет внутри JavaScript runtime браузера и вынужден делить главный поток с обработчиками событий, Promise, таймерами, layout, paint и любым синхронным кодом приложения. Именно поэтому два одинаковых по смыслу обновления UI могут ощущаться по-разному: одно укладывается в кадр, второе блокирует main thread и делает интерфейс вязким.
Если вы уже читали разбор того, когда React действительно перерисовывает компонент, материал про batching обновлений и статью про concurrent rendering, эта тема даст связующий слой. Если нет, полезно держать их рядом: здесь мы не повторяем весь React, а показываем, почему timing среды исполнения меняет поведение React-приложения.
Больше вопросов в Telegram
Ежедневные разборы и реальные кейсы с интервью.
Что такое Event Loop в контексте React
Если убрать лишние абстракции, Event Loop решает одну практическую задачу: в каком порядке среда будет выполнять синхронный код, microtask, macrotask и когда даст браузеру шанс отрисовать кадр.
Для React важны четыре опорные идеи:
- Обработчик события, например
onClick, выполняется синхронно. Promise.thenиqueueMicrotaskпопадают в очередь microtask и обычно выполняются раньше следующей macrotask.setTimeoutпопадает в macrotask и почти всегда будет позже microtask.- Paint не происходит посреди длинного синхронного куска кода. Пока main thread занят, пользователь не увидит обновление.
Из этого вытекает главное: React может принять обновление состояния сейчас, но пользователь увидит результат только тогда, когда среда дойдет до commit, освободит поток и браузер успеет показать кадр. Поэтому обсуждать React без Event Loop так же странно, как обсуждать сеть без очереди запросов.
Отсюда же растут многие вопросы про useEffect, useLayoutEffect, batching и flushSync. Они не про «магические хуки», а про то, в какой момент React и браузер получают право сделать следующий шаг.
Архитектурный разбор: где встречаются Event Loop, Scheduler и React Fiber
Контекст задачи
Представим экран поиска по каталогу. Пользователь печатает в поле, список фильтруется, часть данных догружается, а сверху еще живет аналитика, подсветка совпадений и тяжелые карточки товаров. Симптом простой: ввод иногда подлагивает, хотя сеть не выглядит критичной.
На таком экране есть сразу несколько конкурирующих работ:
- браузер обрабатывает
inputсобытие; - ваш код вызывает
setState; - React планирует render и commit;
- часть логики уходит в
Promise; - после commit запускаются эффекты;
- браузеру еще нужно сделать layout и paint.
Схема компонентов и ролей
Минимальная схема такая:
SearchInputгенерирует события ввода;CatalogPageдержит экранный state;ResultsListдорого рендерит список;- data-layer ходит в API;
- React Scheduler раскладывает обновления по приоритету;
- браузерный Event Loop решает, когда реально исполнять очередной кусок работы.
Поток события
Практически цепочка выглядит так:
- Пользователь вводит символ.
- Браузер кладет задачу на обработку события.
- Ваш
onChangeсинхронно вызываетsetQuery. - React помечает обновление и решает, как его планировать.
- Если в том же тике есть другие обновления, они могут попасть в один batching.
- React выполняет render-работу и commit.
- Браузер получает шанс отрисовать кадр.
- После этого идут эффекты и вторичная работа.
Критичная граница здесь между render/commit и фактической отрисовкой. Если вы в обработчике события запускаете тяжелый синхронный фильтр на 20-30 мс, никакой React не спасет отзывчивость. Event Loop просто не получит паузу, чтобы браузер показал новый кадр.
Узкие места и деградация
Узкое место может быть в разных слоях:
- длинный синхронный обработчик блокирует ввод;
- слишком широкое обновление React-дерева делает commit дорогим;
useEffectсразу после commit повторно грузит поток;- microtask-каскад из
Promise.thenвытесняет более полезную работу; - SSR страница быстро пришла, но гидрация и ранние эффекты задержали интерактивность.
Если нужен соседний контекст по внутреннему движку React, полезен разбор Fiber и механики рендера. Для SSR-части стоит держать рядом материал про hydration.
Microtask, macrotask и React: где разница становится заметной
Чаще всего разработчик замечает разницу не по теории, а по странному поведению кода.
function Example() {
const [value, setValue] = useState(0);
const handleClick = () => {
setValue((v) => v + 1);
Promise.resolve().then(() => {
console.log("microtask after click");
setValue((v) => v + 1);
});
setTimeout(() => {
console.log("macrotask after click");
setValue((v) => v + 1);
}, 0);
console.log("sync handler end");
};
return <button onClick={handleClick}>Value: {value}</button>;
}
Что здесь важно понимать:
- обработчик клика отрабатывает синхронно;
Promise.thenвыполнится раньшеsetTimeout;- в React 18 несколько обновлений из одного логического сценария часто будут объединены через automatic batching;
- итоговый порядок логов и количество commit зависят не только от React, но и от того, в какие очереди ушла работа.
Именно здесь часто ломают объяснение на интервью. Кандидат говорит: «React сначала применяет все setState, потом рисует». Это слишком грубо. Правильнее говорить, что React планирует обновления внутри ограничений Event Loop и старается сгруппировать работу так, чтобы не делать лишние commit.
Еще один показательный случай связан не с кликом, а с useEffect:
function SearchPage() {
const [query, setQuery] = useState("");
const [result, setResult] = useState<string[]>([]);
useEffect(() => {
if (!query) {
setResult([]);
return;
}
let cancelled = false;
Promise.resolve(query)
.then((q) => q.trim())
.then((q) => {
if (!cancelled) {
setResult([q, `${q}-extended`]);
}
});
return () => {
cancelled = true;
};
}, [query]);
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ul>{result.map((item) => <li key={item}>{item}</li>)}</ul>
</>
);
}
Здесь ошибка не в синтаксисе, а в модели ожиданий. Разработчик часто думает, что Promise внутри эффекта делает код «более асинхронным» и безопасным. На практике microtask может только усложнить порядок исполнения, а сам эффект по-прежнему может породить лишние обновления и гонки. По смежной теме помогает разбор сложных вопросов по useEffect.
Прокачай React за 7 дней
20 вопросов и разборов по React Hooks.
Сравнение подходов: где именно планировать работу
| Подход | Когда использовать | Плюс | Ограничение | Типичный сценарий |
|---|---|---|---|---|
| Синхронно в обработчике события | Нужен мгновенный локальный state | Предсказуемый поток | Легко заблокировать кадр | переключатель, controlled input |
Promise или queueMicrotask | Нужно завершить текущий sync flow и выполнить работу до следующей macrotask | Быстро, без лишней задержки таймера | Можно перегрузить microtask-очередь | постобработка данных, координация нескольких обновлений |
setTimeout | Нужен перенос на следующий тик | Разрывает текущую задачу | Добавляет задержку и не лечит архитектуру | отложенный лог, неблокирующая вторичная работа |
startTransition | Обновление не критично для мгновенного отклика | React может понизить приоритет работы | Не ускоряет тяжелый код сам по себе | фильтрация большого списка, вторичный UI |
useEffect после commit | Нужна синхронизация с внешним миром | Отделяет render от side effects | Легко породить каскад обновлений | подписки, fetch, аналитика |
useLayoutEffect | Нужны измерения DOM до paint | Позволяет избежать визуального скачка | Блокирует paint и дороже для UX | позиционирование tooltip, синхронные измерения |
Таблица полезна именно как критерий выбора. Частая ошибка в production не в том, что используют setTimeout, а в том, что им лечат проблему, которая на самом деле вызвана неверной границей состояния или слишком дорогим render.
Разбор производительности: почему Event Loop напрямую влияет на UX React-приложения
Для пользователя не существует «чисто React-проблемы» и «чисто Event Loop-проблемы». Он видит только одно: интерфейс либо откликается, либо нет.
Практический разбор производительности здесь держится на трех вопросах:
- Сколько времени main thread занят без паузы.
- Насколько дорог commit React-дерева.
- Что запускается сразу после commit и не дает странице стать отзывчивой.
Пример типичного промаха: разработчик переносит вычисление из обработчика в Promise.then и считает, что разгрузил интерфейс. Но длинная цепочка microtask тоже выполняется на том же потоке. Если она тяжелая, пользователь все равно не получит кадр вовремя.
Другой частый случай связан с startTransition:
import { startTransition, useState } from "react";
function FilteredList({ items }: { items: string[] }) {
const [query, setQuery] = useState("");
const [filtered, setFiltered] = useState(items);
const onChange = (nextQuery: string) => {
setQuery(nextQuery);
startTransition(() => {
const next = items.filter((item) =>
item.toLowerCase().includes(nextQuery.toLowerCase())
);
setFiltered(next);
});
};
return (
<>
<input value={query} onChange={(e) => onChange(e.target.value)} />
<ul>{filtered.map((item) => <li key={item}>{item}</li>)}</ul>
</>
);
}
Что делает этот код правильно: разделяет срочное обновление поля ввода и менее срочное обновление списка. Что он не делает: не уменьшает стоимость самого filter, если список огромный и вычисление тяжелое. Event Loop по-прежнему обслуживает тот же поток, просто React получает больше свободы в приоритизации.
Отсюда инженерный вывод: оптимизация оправдана, когда вы уже нашли узкое место. Если интерфейс тормозит из-за layout thrashing, сеть или гидрации, микроманипуляции с очередями дадут мало. Для соседней темы полезны React DevTools для дебага и профилирования и материал по Suspense.
Production pitfalls: где связь Event Loop и React ломают чаще всего
1. Лечат лаги через setTimeout, не понимая источник блокировки
Признаки:
- код начинает обрастать таймерами;
- порядок обновлений становится менее предсказуемым;
- баг «почти исчезает», но UX остается нестабильным.
Последствие: проблема не решается, а размазывается по тикам. Если синхронная работа сама по себе тяжелая, она останется тяжелой и в следующей macrotask.
2. Делают тяжелую работу в useEffect, считая, что это бесплатно
Признаки:
- commit завершился, но интерфейс все равно подвисает;
- после mount или route transition CPU резко растет;
- в профиле видно, что эффект сразу запускает дорогие вычисления, listeners или повторные fetch.
Последствие: пользователь видит HTML или уже обновленный DOM, но интерактивность наступает позже, чем ожидалось. Особенно болезненно это выглядит рядом с hydration после SSR.
3. Путают batching с отложенным выполнением
Признаки:
- ожидают, что несколько
setStateвсегда дадут один commit; - не учитывают границы событий, microtask и внешних источников данных;
- логируют state сразу после
setStateи считают React «непредсказуемым».
Последствие: команда начинает бороться не с причиной, а с симптомами. Полезный соседний материал здесь: как работает группировка обновлений в React.
4. Игнорируют стоимость пользовательского JavaScript вне React
Признаки:
- commit у React нормальный, а ввод все равно вязкий;
- сторонняя аналитика или форматтеры текста живут в том же обработчике события;
- локально на сильной машине все «нормально», а на слабом устройстве UX разваливается.
Последствие: React обвиняют в том, что на самом деле создают обычные long tasks в main thread.
Практики, которые обычно работают лучше всего
Архитектурные практики
- Держите срочные обновления отдельно от вторичных. Ввод в поле и пересчет тяжелого списка не обязаны жить в одном приоритете.
- Проектируйте state так, чтобы одно действие пользователя не тащило за собой половину дерева. В этом помогает разбор Context и альтернатив.
- Сначала определяйте слой проблемы: событие, React-дерево, effect, hydration, layout или сеть.
Практики кода
- Не используйте
useEffectкак контейнер для любой логики подряд. - Не вставляйте
Promise.resolve().then(...)только ради ощущения контроля над timing. - Если работа тяжелая, сначала сократите ее объем, а уже потом меняйте приоритет выполнения.
- Для измерений DOM используйте
useLayoutEffectтолько там, где без него реально будет визуальный дефект.
Наблюдаемость и тестирование
- Профилируйте не только React commit, но и long tasks в браузере.
- Повторяйте сценарий на слабом профиле устройства, а не только на локальной машине разработчика.
- Сопоставляйте
Profiler,PerformanceиNetwork, иначе легко лечить не тот слой.
Rollout и откат
- Спорные оптимизации по приоритетам выкатывайте через feature flag.
- Если переходите на
startTransitionили меняете структуру state, сравнивайте метрики route-to-interactive и input latency до и после.
Частые ошибки
- Считать, что React сам управляет Event Loop. Нет, он работает поверх ограничений среды.
- Путать
render,commit,paintиuseEffectкак один неразделимый этап. - Считать
PromiseиsetTimeoutпочти одинаковыми инструментами. - Лечить timing-баги таймерами вместо уменьшения синхронной нагрузки.
- Объяснять concurrent rendering как «React теперь все делает параллельно». Это неверно; главный поток все еще один.
Как отвечать на интервью
Сильный ответ на вопрос «как связаны Event Loop и React» обычно строится так:
- Сначала объясняете, что React исполняется внутри JavaScript runtime браузера и зависит от Event Loop.
- Затем показываете разницу между синхронным кодом, microtask, macrotask, render, commit и эффектами.
- После этого связываете тему с практикой: batching,
useEffect,startTransition, input lag, hydration.
Хорошая короткая формулировка:
React не заменяет Event Loop, а работает внутри него. Обработчики событий, Promise, таймеры и paint конкурируют за один поток. Поэтому timing очередей напрямую влияет на batching, момент commit, запуск useEffect и общую отзывчивость UI.
Если интервьюер просит глубже, продолжайте через конкретный кейс: controlled input, тяжелый список, startTransition, ранний useEffect после commit или проблемную гидрацию. Это звучит сильнее, чем пересказ терминов. Для смежной подготовки полезны частые ошибки на React-собеседовании и как пройти React-собеседование в 2026 году.
Разберите React timing и performance на реальных сценариях
Отработайте вопросы по Event Loop, batching, useEffect, concurrent rendering и debugging в формате mock interview и коротких инженерных разборов.
FAQ
Нужно ли фронтенд-разработчику глубоко знать Event Loop?
Да, если вы работаете не только с версткой, но и с интерактивным UI. Без Event Loop трудно объяснить input lag, порядок выполнения Promise, timing useEffect и стоимость тяжелых обработчиков событий.
Почему после setState лог иногда показывает старое значение?
Потому что setState не означает немедленный синхронный commit на той же строке. React планирует обновление в рамках текущего потока выполнения и batching. Лог сразу после вызова часто отражает текущее замыкание, а не уже примененный результат.
Чем microtask опаснее, чем кажется?
Тем, что она выглядит маленькой и безвредной. Но длинная цепочка microtask тоже занимает main thread. Если в ней дорогие вычисления, UI не станет отзывчивее только потому, что вы убрали setTimeout.
Почему useEffect не подходит для любой логики?
Потому что это инструмент синхронизации с внешним миром после commit, а не универсальное место для вычислений. Если вы переносите туда тяжелую работу без причины, то откладываете, но не устраняете стоимость.
React concurrent rendering делает код параллельным?
Нет. Он дает React возможность гибче прерывать и приоритизировать работу, но браузерный main thread по-прежнему один. Поэтому тяжелый синхронный код все еще способен заблокировать интерфейс.
Итоги
Связь между Event Loop и React практическая, а не академическая. Event Loop определяет, когда исполняются обработчики, microtask и macrotask, а React поверх этого пытается спланировать render и commit так, чтобы интерфейс оставался отзывчивым.
Если свести статью к одной мысли, она такая: большинство timing-проблем в React нельзя понять только через API хуков. Нужна модель потока выполнения. Как только вы начинаете различать событие, очередь, commit, paint и effect, многие «магические» баги перестают быть магическими.
Больше вопросов в Telegram
Ежедневные разборы и реальные кейсы с интервью.
Автор
Lexicon Team
Читайте также
frontend
20 задач по React с разбором: что реально проверяют на собеседовании и в production
Собрали 20 задач по React с разбором: ререндеры, keys, формы, refs, Suspense, Context, SSR, оптимизация и типичные ошибки, которые всплывают на интервью.
frontend
Synthetic Events в React: как работает система событий
Разбираем Synthetic Events в React: делегирование, bubbling/capturing, приоритеты обновлений, интеграция с native events и production-ошибки.
frontend
System design для frontend разработчика: как мыслить системно, а не только компонентами
Практический разбор system design для frontend разработчика: границы ответственности, данные, производительность, ошибки и сильный ответ на интервью.