Event Loop и React: как связаны и почему это влияет на рендер

Разбираем связь Event Loop и React без упрощений: macrotask, microtask, batching, commit, useEffect, scheduler, производительность и типичные ошибки на интервью.

11 марта 2026 г.20 минLexicon Team

Введение

Тема 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 важны четыре опорные идеи:

  1. Обработчик события, например onClick, выполняется синхронно.
  2. Promise.then и queueMicrotask попадают в очередь microtask и обычно выполняются раньше следующей macrotask.
  3. setTimeout попадает в macrotask и почти всегда будет позже microtask.
  4. 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 решает, когда реально исполнять очередной кусок работы.

Поток события

Практически цепочка выглядит так:

  1. Пользователь вводит символ.
  2. Браузер кладет задачу на обработку события.
  3. Ваш onChange синхронно вызывает setQuery.
  4. React помечает обновление и решает, как его планировать.
  5. Если в том же тике есть другие обновления, они могут попасть в один batching.
  6. React выполняет render-работу и commit.
  7. Браузер получает шанс отрисовать кадр.
  8. После этого идут эффекты и вторичная работа.

Критичная граница здесь между 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-проблемы». Он видит только одно: интерфейс либо откликается, либо нет.

Практический разбор производительности здесь держится на трех вопросах:

  1. Сколько времени main thread занят без паузы.
  2. Насколько дорог commit React-дерева.
  3. Что запускается сразу после 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 до и после.

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

  1. Считать, что React сам управляет Event Loop. Нет, он работает поверх ограничений среды.
  2. Путать render, commit, paint и useEffect как один неразделимый этап.
  3. Считать Promise и setTimeout почти одинаковыми инструментами.
  4. Лечить timing-баги таймерами вместо уменьшения синхронной нагрузки.
  5. Объяснять concurrent rendering как «React теперь все делает параллельно». Это неверно; главный поток все еще один.

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

Сильный ответ на вопрос «как связаны Event Loop и React» обычно строится так:

  1. Сначала объясняете, что React исполняется внутри JavaScript runtime браузера и зависит от Event Loop.
  2. Затем показываете разницу между синхронным кодом, microtask, macrotask, render, commit и эффектами.
  3. После этого связываете тему с практикой: 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

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