Event Loop — это основа асинхронности в JavaScript и одна из самых любимых тем на собеседованиях. Если вы понимаете, как он устроен, вам не страшны ни таймер с нулевой задержкой, ни промис, который внезапно выполняется раньше таймера, ни каверзные вопросы интервьюера.
В этой статье разберём, как работает Event Loop, что происходит внутри очереди задач и зачем JavaScript понадобились микро- и макрозадачи.
Что такое Event Loop и зачем он нужен в JavaScript?
JavaScript — однопоточный язык. Он может выполнять только одну задачу в один момент времени, потому что у него есть единственный поток исполнения — так устроен сам язык и его движок (например, V8 в Chrome или Node.js).
Но современный JavaScript не живёт в вакууме — он работает в окружении, будь то браузер или Node.js. И вот именно окружение (а не сам язык) предоставляет механизм Event Loop (цикл событий), который управляет выполнением кода и асинхронными событиями.
Браузер и Node.js создают инфраструктуру, где Event Loop следит за тем, что выполняется прямо сейчас, а что ждёт своей очереди — например, обработка клика, ответ от сервера или выполнение setTimeout.

Event Loop постоянно проверяет, не освободился ли стек вызовов, и если да — забирает следующую задачу из очереди. Именно благодаря этому JavaScript может обрабатывать множество событий без зависаний, даже оставаясь однопоточным.
Основные компоненты: Call Stack, Web APIs, Task Queue, Event Loop
Чтобы понять, как JavaScript справляется с асинхронностью, нужно знать, что там вообще внутри. Всё основано на четырёх компонентах:
- Call Stack — стек вызовов, где выполняется ваш код.
- Web APIs — среда, где обрабатываются асинхронные операции вроде
setTimeout,fetchили событий DOM. - Task Queue (очередь задач, или колбэков) — место, где ждут готовые к выполнению задачи.
- Event Loop — механизм, который следит за всеми тремя и решает, когда брать задачу из очереди и кидать её обратно в стек.

Event Loop по сути диспетчер. Он не выполняет код сам, но управляет порядком выполнения. Стек говорит: «Я занят» или «Свободен», Web APIs накапливают готовые колбэки, а Event Loop следит, когда можно безопасно подбросить их обратно в работу.
Изначально в этой модели была только одна очередь — Task Queue (она же Callback Queue), куда складывались все задачи из setTimeout, событий, fetch и т. д. Но с выходом ES6 в 2015 году в движки добавили ещё одну — Microtask Queue, чтобы выполнять короткие, приоритетные задачи сразу после текущего кода, не дожидаясь следующего цикла. О них мы поговорим чуть позже.
Полезный блок со скидкой
Если вам интересно разбираться со смартфонами, компьютерами и прочими гаджетами и вы хотите научиться создавать софт под них с нуля или тестировать то, что сделали другие, — держите промокод Практикума на любой платный курс: KOD (можно просто на него нажать). Он даст скидку при покупке и позволит сэкономить на обучении.
Бесплатные курсы в Практикуме тоже есть — по всем специальностям и направлениям, начать можно в любой момент, карту привязывать не нужно, если что.
Стек вызовов (Call Stack): как выполняются функции
Call Stack — это структура, где JavaScript хранит информацию о том, какие функции сейчас выполняются и в каком порядке.
Когда вызывается функция, она попадает в стек. Когда заканчивает работу — удаляется. Если функции вызывают друг друга, стек начинает расти: каждая новая ложится поверх предыдущей. JS выполняет их строго сверху вниз — пока текущая функция не завершится, следующая не начнётся.

Посмотрим пример c двумя вложенными функциями:
function one() {
console.log('раз');
}
function two() {
one();
console.log('два');
}
two();

JavaScript выполняет код строка за строкой. Когда движок доходит до вызова two(), он помещает эту функцию в стек и начинает её выполнение. Внутри two() вызывается one() — она кладётся поверх предыдущей в стеке, потому что теперь выполняется именно она.
Движок доходит до console.log(‘раз’), выводит «раз» и завершает one(). Функция убирается из стека. После этого выполнение продолжается с того места, где остановилось — внутри two().
Выполняется console.log(‘два’), функция завершается, стек очищается.

Пока стек не опустеет, никакие другие задачи не запустятся. Если вы случайно запустите бесконечный цикл или тяжёлую синхронную операцию, Event Loop не сможет подкинуть новые задачи и страница просто зависнет.
Web APIs: где живут асинхронные операции и события браузера
JavaScript сам по себе ничего не умеет делать асинхронно. Он не может ждать таймер, тянуть данные из сети или слушать события. Всем этим занимается окружение, то есть Web APIs — часть браузера (или libuv в Node.js), которая работает параллельно с движком JS.

Когда вы вызываете setTimeout, fetch или addEventListener, движок передаёт задачу в Web API и просто идёт дальше — не ждёт, не блокирует и не зависает.
А браузер тем временем ставит таймер, делает HTTP-запрос или слушает событие. Когда результат готов — он аккуратно кладёт колбэк в очередь, чтобы Event Loop потом передал его обратно в JavaScript.
Допустим, есть такой код, в котором реализована логика клика по кнопке:
console.log('Подписываемся на событие');
document.addEventListener('click', () => {
console.log('Клик!');
});
console.log('Готово');
Разберём по шагам, что тут происходит.
- В консоли появляется «Подписываемся на событие» — синхронный код выполняется сразу.
- Движок доходит до
addEventListener(‘click’, …)— он не ждёт клика, а просто регистрирует обработчик и отдаёт управление браузеру. - Теперь сам браузер через Web API слушает событие клика в фоне, пока JS продолжает выполнение.
- Следующая строка
console.log(‘Готово’)выполняется сразу, и в консоли появляется «Готово». - На этом синхронная часть кода заканчивается, и стек очищается.
Когда пользователь нажимает кнопку, браузер сообщает Event Loop, что есть готовая задача. Колбэк из addEventListener попадает в очередь, Event Loop видит, что стек пуст, и выполняет обработчик — в консоли появляется «Клик!».

Как работает Event Loop: пошаговый разбор на примере кода
Дальше посмотрим, что происходит с кодом, когда он попадает в стек, как асинхронные задачи уходят в окружение и в какой момент Event Loop возвращает их обратно в очередь.
Наглядный пример с setTimeout и console.log
Разберём типичную ситуацию, когда код ведёт себя «нелогично»:
console.log('A');
setTimeout(() => console.log('B'), 0);
console.log('C');
В консоли мы увидим:

Несмотря на то что таймер стоит с нулевой задержкой, B выполняется после всех остальных операций.
Почему так?
Когда движок встречает setTimeout, он передаёт задачу в Web API — браузер запускает таймер и «запоминает» колбэк. А сам JavaScript продолжает выполнять код дальше — выводит A, потом C. Когда стек вызовов освобождается, Event Loop видит, что таймер завершён, и возвращает колбэк обратно в очередь. Только после этого в консоли появляется B.
А зачем вообще ставить setTimeout(fn, 0)?
Такой приём используют, когда нужно отложить выполнение кода до следующего оборота Event Loop, не делая настоящей паузы.
Например: вы обновляете DOM и хотите измерить размер элемента. Если сделать это сразу, то браузер может ещё не успеть пересчитать макет. Тогда вы оборачиваете вычисление в setTimeout(fn, 0) — и браузер дорисует всё, прежде чем запустить вашу функцию.
Идея простая: setTimeout(fn, 0) не тормозит код, а даёт браузеру закончить текущие задачи, освободить стек и только потом вернуться к вашему колбэку.
Визуализация процесса: от стека до очереди и обратно
Чтобы по-настоящему понять Event Loop, полезно увидеть, как код реально двигается между стеком, Web API и очередью.
- JS выполняет код в стеке. Всё, что синхронно — функции, вычисления,
console.log, — идёт сюда. Пока стек чем-то занят, больше ничего не выполняется. - Асинхронные операции уходят в Web APIs.
setTimeout,fetch, обработчики событий — все они выполняются за пределами движка. Когда операция завершается (таймер истёк, ответ с сервера пришёл), Web API кладёт колбэк в очередь. - Task Queue ждёт своей очереди. Он как зал ожидания: готовые задачи стоят в ряд и ждут, пока стек освободится.
- Event Loop проверяет состояние. Если стек пуст, то Event Loop берёт первую задачу из очереди и кидает её обратно в стек. Потом снова ждёт, пока стек не опустеет, — и процесс повторяется.
И так по кругу, бесконечно.
Есть несколько классных инструментов, где Event Loop можно посмотреть в действии:
Loupe — визуализатор Event Loop. Можно вставить свой код и пошагово увидеть, как функции переходят из стека в Web API и обратно.

JS Visualizer 9000 — интерактивная анимация Event Loop, показывает микрозадачи и макрозадачи.

В DevTools, во вкладке «Производительность» (Performance), можно записать выполнение и увидеть, где движок занят JS (жёлтые блоки), а где ждёт событий:

Микрозадачи (Microtasks) и макрозадачи (Macrotasks): в чём разница?
Всё, что делает JavaScript асинхронным, проходит через очередь задач.
Но штука в том, что очередей на самом деле две: микрозадачи и макрозадачи. И микрозадачи всегда пролезают вне очереди, а макрозадачи ждут.

Очередь микрозадач появилась в 2015 году вместе с промисами. Это было ответом на растущую сложность интерфейсов и необходимость обрабатывать короткие асинхронные операции быстрее — без лагов и лишних циклов Event Loop.
Что относится к микрозадачам (Promise, queueMicrotask)
Микрозадачи — это мелкие, но приоритетные задачи, которые выполняются сразу после текущего стека, до любых таймеров или событий. Они нужны, чтобы быстро «дожать» кусок логики, не дожидаясь следующего оборота Event Loop.
Типичные микрозадачи из реальных сценариев:
- Промисы (
Promise.then(), catch(), finally()) — используются для асинхронных операций, когда нужно выполнить код после завершения запроса, вычисления или анимации. Например,вы загрузили данные с API и хотите обновить интерфейс, когда они придут, —.then()отработает как микрозадача, сразу после текущего кода; queueMicrotask()— прямой способ добавить задачу в очередь микрозадач. Например, вы хотите вызвать функцию после текущего стека, но не дожидатьсяsetTimeout.;
queueMicrotask(() => console.log(‘Это микрозадача’))- MutationObserver — API браузера, который отслеживает изменения в DOM. Когда DOM изменился, колбэк
MutationObserverвыполняется как микрозадача — то есть быстро, но уже после основного кода.
Что относится к макрозадачам (setTimeout, setInterval, I/O)
Макрозадачи — это всё, что приходит из внешнего мира: таймеры, клики, сетевые ответы. Каждая макрозадача — это новый оборот Event Loop, между которыми движок успевает выполнить все микрозадачи.
Типичные макрозадачи из реальных сценариев:
setTimeout,setInterval— таймеры и периодические проверки. Например, обновление времени на странице раз в секунду.- События DOM (
click,scroll,keydown) — пользователь кликает по кнопке, скроллит страницу или нажимает клавишу. Каждый такой обработчик попадает в очередь макрозадач. - Сетевые запросы (
fetch,XMLHttpRequest) — после получения ответа с сервера движок ставит колбэк в очередь макрозадач. - Операции ввода-вывода (I/O) — чтение и запись файлов, работа с потоками или сокетами в Node.js.
Приоритет выполнения: почему микрозадачи важнее макрозадач?
Когда движок JavaScript завершает выполнение текущего стека, он не спешит сразу переходить к таймерам или событиям. Сначала он выгребает все микрозадачи, которые накопились, только после этого берёт первую макрозадачу.
Такой порядок был введён не случайно: микрозадачи — это короткие, внутренние операции, которые часто нужны для корректной работы самого кода. Например, чтобы завершить цепочку промисов или обновить состояние после асинхронного шага, не дожидаясь новой итерации Event Loop.
Если бы движок выполнял их после макрозадач, промисы и их .then() реагировали бы с задержкой: интерфейс притормаживал бы, а синхронность между частями кода ломалась.
Теперь же порядок строгий:
- Выполняется стек синхронного кода.
- Потом все микрозадачи (
Promise,queueMicrotask). - Потом одна макрозадача (
setTimeout, событие,fetch). - И цикл повторяется.
Так JavaScript остаётся отзывчивым даже при сложных интерфейсах и большом количестве асинхронных событий.
👉 Если вы хотите глубже разобраться в конкретных механизмах, почитайте наши статьи про Промисы и setTimeout() — там мы подробно объясняем, как они работают под капотом.
Подробный пример: порядок выполнения Promise и setTimeout
Теперь соберём всё вместе и посмотрим, как Event Loop работает, когда микрозадачи и макрозадачи пересекаются. Рассмотрим задачу, которую вполне могут дать на собеседовании.
Итак, что в каком порядке выведется?
console.log('Начало');
const promise1 = Promise.resolve().then(() => {
console.log('Промис 1');
const timer2 = setTimeout(() => {
console.log('Таймер 2');
}, 0);
});
const timer1 = setTimeout(() => {
console.log('Таймер 1');
const promise2 = Promise.resolve().then(() => {
console.log('Промис 2');
});
}, 0);
console.log('Конец');
Выглядит запутанно, но если знать, как работает Event Loop — всё будет просто. Разбираем по шагам.
Сначала движок читает код построчно и выполняет всё, что можно сделать сразу.
- Выполняется
console.log(‘Начало’)— в консоли появляется«Начало». - Движок доходит до
Promise.resolve().then(…). Сам промис создаётся мгновенно, а его колбэк (Промис 1) добавляется в очередь микрозадач — он выполнится чуть позже. - Далее встречается
setTimeout(…)с колбэкомТаймер 1— он уходит в Web API и ждёт своей очереди как макрозадача. - Затем выполняется последняя синхронная строка —
console.log(‘Конец’). В консоли появляется«Конец».
К этому моменту у нас:
- стек синхронных вызовов уже пуст;
- в очереди микрозадач лежит
Промис 1; - в очереди макрозадач —
Таймер 1; - а внутри
Промиса 1уже запланировано созданиеТаймера 2, который попадёт туда же чуть позже.
После этого Event Loop начинает разбирать всё, что накопилось:
- Выполняет
Промис 1(микрозадача) — в консоль летит«Промис 1»и создаётсяТаймер 2. - Затем Event Loop переходит к первой макрозадаче —
Таймер 1. В отправляется летит«Таймер 1», а внутри создаётся новыйПромис 2(микрозадача). - После Таймера 1 движок обрабатывает микрозадачу
Промис 2и выводится«Промис 2». - И только потом доходит очередь до
Таймера 2, и в консоли появляется«Таймер 2».
Поэтому итоговый вывод должен быть таким:
Начало
Конец
Промис 1
Таймер 1
Промис 2
Таймер 2
Проверяем:

Еще раз подытожим:
- Промис — это микрозадача → выполняется сразу после текущего стека.
setTimeout— это макрозадача → ждёт следующего оборота Event Loop.- После выполнения каждой макрозадачи (таймера или события) движок проверяет очередь микрозадач.
- Если в ней что-то накопилось — все микрозадачи выполняются сразу, прежде чем начнётся следующая макрозадача.
Поэтому Promise почти всегда выполняется раньше таймера, даже если тот поставлен на 0 миллисекунд, поскольку так устроена архитектура самого Event Loop.
Бонус для читателей
Если вам интересно погрузиться в мир ИТ и при этом немного сэкономить, держите наш промокод на курсы Практикума. Он даст вам скидку при оплате, поможет с льготной ипотекой и даст безлимит на маркетплейсах. Ладно, окей, это просто скидка, без остального, но хорошая.