Перейти к содержанию
Innovialabs

Как работает event loop в браузере: задачи, микрозадачи, рендеринг

Продвинутое3 мин

Коротко

Браузер берёт одну задачу, выполняет её до конца, затем опустошает всю очередь микрозадач и только потом может отрисовать кадр; пока работает ваш код, страница не перерисовывается и не реагирует на клики.

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

У страницы один основной поток. На нём выполняется JavaScript, обрабатываются события и происходит отрисовка. Event loop — это цикл, который решает, что делать дальше:

  1. Взять одну задачу (task) из очереди и выполнить её целиком.
  2. Выполнить все микрозадачи (microtasks), включая те, что появились по ходу.
  3. Если пришло время кадра — выполнить колбэки requestAnimationFrame, пересчитать стили и раскладку и отрисовать страницу.
  4. Повторить.

Отсюда главное правило: пока выполняется ваш код, браузер не рисует и не обрабатывает клики.

Стек вызовов

Когда функция вызывается, она кладётся в стек вызовов (call stack); когда возвращает результат — снимается. Event loop берёт следующую задачу только когда стек пуст. Длинная синхронная функция держит стек занятым, и всё остальное ждёт.

Задачи и микрозадачи

Задачи (tasks)Микрозадачи (microtasks)
ОткудаsetTimeout, setInterval, события пользователя, сетевые колбэки, MessageChannelPromise.then/catch/finally, await, queueMicrotask, MutationObserver
Сколько за разОдна за оборот циклаВся очередь до конца
Рендеринг между нимиВозможенНевозможен

Задачка 1: порядок вывода

console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");

Результат: 1, 4, 3, 2.

  • 1 и 4 — синхронный код текущей задачи.
  • 3 — микрозадача: выполняется сразу после того, как текущая задача закончилась.
  • 2 — новая задача: ждёт следующего оборота цикла. setTimeout(fn, 0) означает «не раньше чем», а не «немедленно».

Задачка 2: почему спиннер не крутится

button.addEventListener("click", () => {
  spinner.hidden = false;
  heavyCalculation(); // несколько секунд синхронной работы
  spinner.hidden = true;
});

Спиннер так и не появится. DOM меняется, но отрисовка произойдёт только после завершения обработчика, а к тому моменту спиннер уже снова скрыт. Всё это время страница заморожена.

Задачка 3: микрозадачи тоже могут заморозить

function loop() {
  Promise.resolve().then(loop);
}
loop();

Каждая микрозадача ставит новую, а очередь микрозадач выполняется до конца. Браузер никогда не дойдёт до рендеринга — вкладка зависнет так же, как от while (true). С setTimeout(loop, 0) вкладка останется живой: между задачами есть шанс отрисовать кадр.

Где место requestAnimationFrame

requestAnimationFrame(callback) вызывает функцию прямо перед отрисовкой следующего кадра. Поэтому анимации и измерения, которые должны совпасть с кадром, делают в нём, а не в setTimeout:

  • колбэки rAF синхронизированы с частотой обновления экрана;
  • в фоновой вкладке они обычно приостанавливаются и не тратят ресурсы;
  • изменения стилей из нескольких rAF-колбэков попадают в один кадр.

Как не замораживать интерфейс

  • Разбивайте длинную работу на части и отдавайте управление между ними — через setTimeout или scheduler.yield() там, где он поддерживается. Так браузер успевает обработать клики и отрисовать кадр.
  • Тяжёлые вычисления — в Web Worker. Воркер работает в отдельном потоке и общается с основным через сообщения.
  • Не смешивайте чтение и запись раскладки в цикле: чтение offsetHeight после изменения стилей заставляет браузер синхронно пересчитать layout.
  • Обновляйте UI до тяжёлой работы, если нужно показать индикатор: сначала покажите его, дождитесь кадра, затем запускайте вычисления.
async function run() {
  spinner.hidden = false;
  await new Promise((r) => requestAnimationFrame(() => setTimeout(r, 0)));
  heavyCalculation();
  spinner.hidden = true;
}

Ожидание rAF и затем setTimeout гарантирует, что кадр со спиннером успеет отрисоваться. Но интерфейс всё равно замрёт на время расчёта — по-настоящему решает задачу только Web Worker или разбиение работы на части.

Как это увидеть

Вкладка Performance в DevTools показывает задачи на основном потоке. Задачи длиннее примерно 50 мс помечаются как long tasks — именно они делают интерфейс неотзывчивым и ухудшают метрику INP.

Частые вопросы

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

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

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

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.