Что такое event loop в Node.js?
Что такое Event Loop
Event loop (цикл событий) — это фундаментальный механизм среды выполнения Node.js, реализованный через библиотеку libuv. Он позволяет однопоточному JavaScript обрабатывать тысячи параллельных операций ввода-вывода без блокировки основного потока.
Node.js не ждёт завершения медленных операций (чтение файла, сетевой запрос, запрос к БД). Вместо этого он регистрирует колбэк и продолжает выполнять другой код. Когда операция завершается, колбэк помещается в очередь, и event loop его вызывает.
Фазы Event Loop
Event loop работает циклически и проходит через 6 фаз по порядку:
1. timers
Выполняются колбэки setTimeout и setInterval, у которых истёк заданный порог времени.
2. pending callbacks
Выполняются I/O-колбэки, отложенные с предыдущей итерации (например, ошибки TCP).
3. idle, prepare
Внутреннее использование libuv, недоступно разработчику.
4. poll
Главная фаза: Node.js забирает новые I/O-события из очереди и выполняет их колбэки. Если очередь пуста — ждёт новых событий (блокируется до таймаута).
5. check
Выполняются колбэки setImmediate.
6. close callbacks
Выполняются колбэки закрытия (socket.on('close', ...))
Микрозадачи (Microtasks)
Помимо фаз существуют очереди микрозадач, которые выполняются между каждыми двумя фазами и имеют приоритет над макрозадачами:
process.nextTick— выполняется первой, перед PromisePromise.resolve().then(...)— выполняется после nextTick
console.log('1: синхронный код');
setTimeout(() => console.log('5: setTimeout'), 0);
setImmediate(() => console.log('4: setImmediate'));
Promise.resolve().then(() => console.log('3: Promise microtask'));
process.nextTick(() => console.log('2: nextTick'));
// Вывод: 1 → 2 → 3 → 4 → 5
setTimeout(fn, 0) vs setImmediate
// Вне I/O контекста порядок не гарантирован
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// Внутри I/O колбэка setImmediate всегда идёт первым
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0); // второй
setImmediate(() => console.log('immediate')); // первый — всегда
});
Блокировка Event Loop
Длинные синхронные вычисления блокируют event loop и не дают обрабатывать новые запросы:
// Плохо: блокирует event loop на всё время вычисления
app.get('/bad', (req, res) => {
let sum = 0;
for (let i = 0; i < 1_000_000_000; i++) sum += i; // блокировка!
res.json({ sum });
});
// Хорошо: выносим тяжёлые вычисления в Worker Thread
import { Worker } from 'worker_threads';
app.get('/good', (req, res) => {
const worker = new Worker('./heavy-task.js');
worker.on('message', (result) => res.json({ result }));
});
Практический вывод
Понимание event loop помогает:
- избегать блокирующего кода на критических путях
- правильно выбирать между
setImmediate,setTimeoutиprocess.nextTick - понимать порядок выполнения асинхронного кода при дебаге
Что хочет услышать интервьюер
Объяснение однопоточности Node.js и роли libuv в делегировании I/O операционной системе
Знание основных фаз event loop (timers → poll → check) и их назначения
Понимание разницы между микрозадачами (nextTick, Promise) и макрозадачами (setTimeout, setImmediate)
Осознание, что блокировка event loop синхронным кодом — критическая проблема для производительности
Практический пример или демонстрация порядка выполнения асинхронных колбэков
Пример: Порядок выполнения: nextTick → Promise → setImmediate → setTimeout
console.log('старт');
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
Promise.resolve().then(() => console.log('Promise.then'));
process.nextTick(() => console.log('nextTick'));
console.log('конец синхронного кода');
// Порядок вывода:
// старт
// конец синхронного кода
// nextTick ← микрозадача с наивысшим приоритетом
// Promise.then ← микрозадача
// setImmediate ← фаза check
// setTimeout ← фаза timers (может меняться местами с setImmediate вне I/O)
Пример: Опасность блокировки event loop и решение через Worker Threads
import { Worker, isMainThread, parentPort } from 'worker_threads';
import express from 'express';
const app = express();
// Плохо: блокирует event loop — все запросы встают в очередь
app.get('/sync-heavy', (_req, res) => {
let result = 0;
for (let i = 0; i < 2_000_000_000; i++) result += i;
res.json({ result });
});
// Хорошо: тяжёлая работа в отдельном потоке, event loop свободен
app.get('/async-heavy', (_req, res) => {
const worker = new Worker(
`
const { parentPort } = require('worker_threads');
let result = 0;
for (let i = 0; i < 2_000_000_000; i++) result += i;
parentPort.postMessage(result); // отправляем результат в главный поток
`,
{ eval: true }
);
worker.once('message', (result) => res.json({ result }));
worker.once('error', (err) => res.status(500).json({ error: err.message }));
});
app.listen(3000);
Типичные ошибки
Думают, что Node.js многопоточный из-за способности обрабатывать много запросов одновременно
Путают порядок: считают, что Promise и process.nextTick выполняются в порядке регистрации вместе с setTimeout
Не знают, что process.nextTick имеет приоритет выше Promise и может рекурсивно заблокировать event loop
Смешивают фазу poll с таймерами — не понимают, почему setTimeout(fn, 0) не всегда самый быстрый
Не могут объяснить, почему длинный цикл for внутри обработчика запроса блокирует все остальные запросы


