Node не выполняет JavaScript параллельно в нескольких потоках. Основной код идет в одном потоке V8, а ожидание сети, файлов и таймеров организует libuv. Понимать эту границу нужно, чтобы не принять очередь колбэков за параллелизм.
Две части одного процесса
V8 разбирает и выполняет JavaScript. У него нет API файлов и сети. Эти возможности дает Node через свои C++-биндинги и libuv.
Сетевые сокеты libuv обслуживает неблокирующе: операционная система сообщает о готовых соединениях, а Node вызывает соответствующие колбэки. Для части файловых операций, DNS getaddrinfo, сжатия и криптографии используется пул потоков libuv. По умолчанию в нем четыре потока, но это не потоки выполнения вашего JavaScript.
Фазы цикла
Event loop состоит из фаз, а не из одной очереди:
- timers - колбэки истекших
setTimeoutиsetInterval; - pending callbacks - отложенные системные колбэки;
- idle и prepare - внутренняя работа Node;
- poll - ожидание и обработка ввода-вывода;
- check - колбэки
setImmediate; - close callbacks - обработчики закрытия ресурсов.
Между отдельными колбэками Node обрабатывает очередь process.nextTick, а затем микрозадачи промисов. Поэтому длинная цепочка nextTick способна не пустить цикл к сети вообще.
живой пример
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
console.log('sync');
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Сначала будут sync, nextTick и promise. Порядок timeout и immediate в главном модуле не стоит использовать как контракт: он зависит от момента запуска. Внутри колбэка ввода-вывода setImmediate выполняется раньше таймера следующего цикла.
Что блокирует сервер
Синхронный колбэк не вытесняется. Пока он считает, Node не принимает другие запросы, не обрабатывает таймеры и не отвечает health check.
function fibonacci(n) {
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}
app.get('/report', () => {
console.log(fibonacci(45));
});
Такой расчет блокирует процесс целиком. Его выносят в worker_threads, а не в пул libuv: пул выполняет только нативные операции. Для тяжелого JSON, шаблонов и вычислений над большими массивами действует то же правило.
Коротко
- V8 выполняет JavaScript, libuv организует ввод-вывод и нативные задачи.
- Event loop состоит из фаз,
nextTickи микрозадачи идут между колбэками. - Порядок
setTimeout(0)иsetImmediateв главном модуле не гарантирован. - Длительный синхронный код блокирует все соединения процесса.
- Тяжелые вычисления выносят в workers, сетевое ожидание само по себе не требует дополнительных потоков.
Что почитать дальше
- Worker threads - как вынести расчет из основного потока.
- Event-loop lag - как измерить задержку цикла.