Event Loop в Node.js: фазы и порядок выполнения
✓ Все примеры выполнены на Node.js v22.23.1 (LTS), июль 2026
Содержание
Событийный цикл — то, что отличает Node.js от «просто JavaScript на сервере». Понять его порядок один раз — значит перестать удивляться, почему код выполняется не так, как написан.
Что это
Событийный цикл — бесконечный цикл в библиотеке libuv, который на каждом обороте проверяет: какие колбэки готовы выполниться? Ваш JavaScript выполняется в одном потоке, а libuv управляет тем, когда какой колбэк вызвать.
Цикл — не один шаг, а шесть фаз, идущих строго по кругу:
| Фаза | Что выполняет |
|---|---|
| timers | колбэки setTimeout и setInterval, чей срок вышел |
| pending callbacks | отложенные системные колбэки |
| idle, prepare | внутреннее, для вас невидимо |
| poll | пришедший ввод-вывод: сеть, файлы |
| check | колбэки setImmediate |
| close callbacks | обработчики закрытия: socket.on('close') |
Между фазами и после каждого колбэка цикл разбирает две очереди с более высоким приоритетом: process.nextTick и микрозадачи (промисы).
-
1
timersСработавшие
setTimeout/setInterval - 2 pollВвод-вывод: данные из сети и файлов
-
3
check
setImmediate -
4
close
socket.on('close')и подобное - 5 → nextTick + микрозадачиРазбираются ПОСЛЕ каждого шага, а не в конце круга
Порядок выполнения: замер
Теорию проверим. Запланируем всё сразу и посмотрим фактический порядок:
setTimeout(() => console.log('setTimeout(0)'), 0);
setImmediate(() => console.log('setImmediate'));
Promise.resolve().then(() => console.log('промис'));
queueMicrotask(() => console.log('queueMicrotask'));
process.nextTick(() => console.log('process.nextTick'));
console.log('синхронный код');
синхронный код -> process.nextTick -> промис -> queueMicrotask -> setImmediate -> setTimeout(0)
Читаем по приоритету:
- синхронный код — выполняется весь, до последней строки;
process.nextTick— своя очередь, разбирается первой;- промисы и
queueMicrotask— очередь микрозадач, в порядке постановки; setImmediate— фаза check;setTimeout(0)— фаза timers.
Почему промис всегда раньше таймера
Это следствие устройства цикла, а не случайность. Очередь микрозадач разбирается полностью после каждого колбэка и между фазами — то есть раньше, чем цикл вообще дойдёт до фазы timers.
Поэтому даже setTimeout(fn, 0) выполнится позже любого промиса: ноль — это минимальная задержка до фазы timers, а не команда «выполни немедленно». Практический вывод: если нужно отложить код на «сразу после текущего», берите queueMicrotask или Promise.resolve().then(), а не setTimeout(0).
Ловушка: порядок nextTick зависит от контекста
Расхожее «process.nextTick всегда первый» верно только на чистом верхнем уровне. Внутри async-функции (после await) порядок меняется: новые микрозадачи попадают в очередь, которая разбирается прямо сейчас, и обгоняют nextTick.
чистый top-level : nextTick -> промис
после top-level await : промис -> nextTick
Поэтому не стройте логику на приоритете nextTick перед промисами. Он зависит от того, откуда вызван код, а это меняется при любом рефакторинге, добавляющем await выше по стеку. В прикладном коде nextTick почти никогда и не нужен.
Главное следствие: блокировка
Пока выполняется ваш синхронный код, цикл стоит целиком. Ни таймеры, ни ввод-вывод, ни промисы не обрабатываются, пока функция не вернёт управление.
Мы замеряли это на счётном цикле: 234 мс вычислений — и ноль тиков 10-миллисекундного таймера. И на readFileSync: 269 мс — тоже ноль. Природа разная — процессор и диск, — а вывод один: всё, что занимает единственный поток, останавливает весь сервер.
Отсюда правило: тяжёлые вычисления выносят в worker_threads, внешние программы — в дочерний процесс. Событийный цикл силён, пока его не заставляют считать.
Один поток — но пул под капотом
Последнее, что снимает путаницу. «Node однопоточный» — про ваш JavaScript. Но libuv держит пул потоков (по умолчанию 4) для файловых операций и DNS. Поэтому await readFile() не блокирует цикл: чтение с диска выполняет отдельный поток, а ваш код получает управление обратно сразу.
Событийный цикл дирижирует, libuv исполняет ввод-вывод в фоне — вместе они и дают модель «не простаивать на ожидании».
Смежные темы: что такое Node.js — общая картина; промисы и async/await — микрозадачи на практике; таймеры; worker_threads. Полный список — в справочнике по Node.js.
Частые вопросы
Почему промис выполняется раньше setTimeout(0)?
setTimeout — макрозадача фазы timers. Очередь микрозадач разбирается полностью после каждого колбэка и между фазами, то есть раньше, чем цикл дойдёт до таймеров.process.nextTick — это микрозадача?
nextTick своя очередь с ещё более высоким приоритетом, чем у промисов: она разбирается раньше очереди микрозадач. Но на верхнем уровне это работает так только вне промис-континуации — внутри неё порядок другой.Чем setImmediate отличается от setTimeout(0)?
setImmediate выполняется в фазе check, setTimeout(0) — в фазе timers. Внутри колбэка ввода-вывода setImmediate всегда раньше; на верхнем уровне порядок между ними не гарантирован.