← назад к разделу

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 способна не пустить цикл к сети вообще.

live example

setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
console.log('sync');
Run

Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →

Сначала будут 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 - как измерить задержку цикла.