⏱ Event Loop, микротаски, макротаски, requestAnimationFrame
💡 Основная идея
Event Loop не выполняет код. Это диспетчер: он проверяет, пуст ли стек вызовов, и затем перемещает следующую задачу из очереди в него.
Два вопроса, на которые отвечает эта заметка: почему setTimeout(fn, 0) всегда выполняется после всего синхронного кода и всех промисов? И почему бесконечная цепочка Promise.then() намертво замораживает вкладку, а бесконечный цикл requestAnimationFrame — нет?
🧭 Участники
- Call Stack (стек вызовов) — выполняет синхронный JS построчно. Пока он не пуст, ничего другого не выполняется.
- JS-движок (V8 и т.д.) — выполняет только стек вызовов. У него нет встроенного понятия
setTimeout, DOM илиfetch— ничего из этого не входит в спецификацию ECMAScript. - Web API (браузер) / libuv (Node.js) — отдельная часть рантайма, которая фактически отсчитывает таймеры, слушает сетевые/DOM-события и подготавливает кадры.
- Очередь макротасков —
setTimeout,setInterval, DOM-события, I/O. FIFO; Event Loop извлекает из неё по одной задаче за раз. - Очередь микротасков —
Promise.then,queueMicrotask,MutationObserver. FIFO, но Event Loop полностью опустошает её, прежде чем делать что-либо ещё. - Event Loop — сам диспетчер: в цикле проверяет «пуст ли стек вызовов?», извлекает следующую задачу, помещает её в стек.
🔑 Золотое правило
После каждой синхронной задачи Event Loop полностью опустошает очередь микротасков — включая микротаски, добавленные во время самого опустошения. Только когда очередь пуста, он переходит дальше (к рендерингу или к одному макротаску).
Разница не в «приоритете» — а в разной стратегии опустошения: микротаски означают «заверши всё, что накопилось, сколько бы там ни оказалось»; макротаск означает «возьми ровно одну вещь и верни управление диспетчеру».
Одна полная итерация цикла:
Call Stack (весь синхронный код)
→ полностью опустошить очередь микротасков (даже если она пополняется во время опустошения)
→ колбэки requestAnimationFrame (снимок ровно этого кадра)
→ Render: Layout → Paint → Composite
→ взять ОДИН макротаск → выполнить его
→ снова опустошить очередь микротасков
→ ... повторить
🎞 requestAnimationFrame
requestAnimationFrame(cb) просит браузер «выполнить это прямо перед отрисовкой следующего кадра». Это не таймер, и он не даёт гарантий по точным миллисекундам — он привязан к реальному циклу обновления экрана (~60/сек).
Порядок выполнения с учётом rAF:
Promise.resolve().then(() => console.log("promise"))
requestAnimationFrame(() => console.log("raf"))
setTimeout(() => console.log("timeout"), 0)
// promise → raf → timeout❄️ Почему бесконечный Promise.then() замораживает вкладку, а бесконечный rAF — нет
Это не совпадение — обе очереди работают по принципиально разной механике.
Очередь микротасков проверяется динамически. Перед каждым шагом Event Loop спрашивает «очередь пуста?». Если выполняющийся сейчас микротаск добавляет в очередь новый, тот присоединяется к очереди и выполняется в этом же проходе. Для бесконечной цепочки условие выхода («пусто») никогда не наступает:
function loop() {
Promise.resolve().then(loop) // каждый .then() порождает следующий
}
loop()
// Event Loop никогда не доходит до рендеринга, макротасков или кликов — вкладка мертваОчередь rAF для кадра — это снимок (батч), зафиксированный в начале обработки этого кадра. Всё, что регистрируется через requestAnimationFrame(...) во время выполнения текущего батча, физически не может попасть в этот же батч — только в следующий кадр:
function animate() {
x++
requestAnimationFrame(animate) // попадёт в СЛЕДУЮЩИЙ кадр, не в этот
}
requestAnimationFrame(animate)
// Render (Layout → Paint → Composite) гарантированно происходит между кадрамиПоэтому бесконечная рекурсия через rAF не блокирует браузер: рендер всегда происходит между итерациями. Ключевое различие не в том, насколько быстро выполняется каждый колбэк — а в том, что у очереди микротасков нет границы-снимка, а у батча rAF она есть.
🧠 Одним предложением
Условие выхода Event Loop для очереди микротасков — «пусто», а самопополняющаяся очередь никогда его не достигает. Очередь rAF же выходит через снимок: всё, что добавляется во время батча, откладывается на следующий, независимо от скорости выполнения.
Browser Rendering Pipeline — что происходит после опустошения очереди микротасков browser