awesome-everything EN
↑ Обратно к восхождению

Браузер и фронтенд-рантайм

Event loop Node.js: фазы, nextTick и задержка цикла

Суть Как шестифазный цикл Node на libuv отличается от браузерного — приоритет process.nextTick, setImmediate vs setTimeout(0), блокировка цикла и измерение задержки event loop.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min

Ваш Node-сервис отлично обрабатывает 1000 запросов в секунду на стейджинге. В продакшене иногда выдаёт хвостовую задержку 500 мс. DevTools бесполезен — у Node нет кадров, нет INP. Нужен эквивалентный диагностический инструмент для headless-цикла: задержка event loop.

Цикл libuv Node: шесть фаз

Управляемый libuv цикл Node проходит шесть фаз за итерацию:

ФазаЧто выполняется
timersИстёкшие колбэки setTimeout / setInterval
pending callbacksОтложенные колбэки ошибок I/O
idle / prepareВнутреннее использование libuv
pollБлокировка на I/O (новые данные сокета, чтение файлов)
checkКолбэки setImmediate
closeСобытия close (например, socket.destroy())

Между каждой фазой дренируются две очереди: очередь process.nextTick (специфичная для Node, срабатывает до микрозадач) и стандартная очередь микрозадач (Promise.then, queueMicrotask).

Приоритет очередей цикла Node
1-й — синхронный код
выполняется до завершения
2-й — process.nextTick
дренируется между каждой фазой
3-й — микрозадачи (Promise.then)
дренируются между каждой фазой
4-й — фаза timers
setTimeout / setInterval
5-й — фаза check
setImmediate
6-й — фаза close
события close

process.nextTick — колбэк с наивысшим приоритетом в Node

process.nextTick — колбэк с наивысшим приоритетом кроме синхронного кода. Срабатывает до микрозадач — до Promise.then. Неправильное использование вызывает тот же паттерн голодания, что и неуправляемые микрозадачи в браузере, но на один уровень приоритета выше:

function tick() {
  process.nextTick(tick); // голодание на уровне nextTick
}
tick(); // колбэки I/O, setImmediate, таймеры никогда не запустятся

Если нужно «запустить после этого синхронного блока», в Node предпочтительнее queueMicrotask или Promise.resolve().then — они имеют правильный приоритет относительно I/O.

setImmediate vs setTimeout(0) в Node

Несмотря на имена, setImmediate не запускается немедленно, а setTimeout(0) — не с нулевой задержкой.

  • setImmediate запускается в фазе check (после I/O текущей итерации).
  • setTimeout(0) запускается в фазе timers следующей итерации.

Внутри I/O-колбэка (чтение файла, чтение сокета) setImmediate всегда выигрывает — он срабатывает в фазе check текущей итерации до фазы timers следующей итерации. Вне I/O-колбэков порядок недетерминирован по спецификации. Код, полагающийся на порядок между этими двумя, хрупок; если нужен гарантированный порядок — используйте queueMicrotask (всегда раньше обоих) или явные зависимости.

Викторина

Внутри I/O-колбэка что срабатывает первым — `setImmediate(fn)` или `setTimeout(fn, 0)`, запланированные из этого колбэка?

Викторина

Обработчик запроса Node делает синхронный `JSON.parse` тела 3 МБ. Какое влияние на другие запросы?

Почему блокировка цикла Node важна

У Node нет концепции кадров или ввода — но он однопоточный, как браузер. JSON.parse на 500 мс в обработчике запроса блокирует каждый другой запрос на том же процессе на 500 мс. Нет защитного барьера рендеринга. Решения:

  • Перенести работу, зависящую от CPU, в Worker thread (node:worker_threads).
  • Делать обработчики запросов async-first.
  • Использовать стриминговые парсеры (JSONStream, oboe.js) для больших тел.

Та же дисциплина «более короткие задачи», которая лечит INP браузера, лечит хвостовую задержку Node.

Измерение задержки event loop в Node

У браузера есть INP и LoAF. У Node есть задержка event loop — эквивалентный диагностический инструмент для headless-цикла. Идея: запланировать таймер на T мс и измерить, насколько поздно он фактически сработал; превышение над T — это время, когда цикл был заблокирован синхронной работой.

perf_hooks.monitorEventLoopDelay() даёт гистограмму этой задержки с перцентилями. Здоровый процесс держит задержку p99 в однозначных мс; p99 в сотнях мс означает, что какой-то обработчик регулярно монополизирует поток, и каждый запрос, попадающий в это окно, получает хвостовую задержку.

const { monitorEventLoopDelay } = require('perf_hooks');
const h = monitorEventLoopDelay({ resolution: 10 });
h.enable();
// … позже:
console.log(`p99 задержка: ${h.percentile(99)}мс`);

Это то же измерение, что INP в браузере — «как долго одна задача держала единственный поток выполнения» — только выраженное как задержка, а не задержка взаимодействия.

Викторина

В Node `process.nextTick(fn)` выполняется до `Promise.resolve().then(fn)`. Почему этот порядок важен?

Почему это работает

Различия в таймингах браузер-vs-Node вызывают продакшен-баги. Модуль, протестированный только в Node, может полагаться на срабатывание process.nextTick до Promise.then. Тот же код в браузере (где nextTick не существует) может использовать полифил, отображающий его на queueMicrotask — который имеет тот же приоритет, что и Promise.then. Код, чередующий nextTick и Promise.then и ожидающий специфичного порядка, даст разные результаты в каждой среде. Тестируйте предположения об асинхронном порядке в обеих средах, а не только в одной.

Вспомните перед уходом
  1. 01
    Перечислите шесть фаз event loop Node по порядку.
  2. 02
    Почему process.nextTick(fn) — ловушка по сравнению с queueMicrotask(fn)?
  3. 03
    Задержка event loop p99 сервиса Node выросла с 5 мс до 200 мс после нового релиза. Вероятная причина и как её найти?
Итог

Event loop Node управляется libuv через шесть упорядоченных фаз: timers, pending callbacks, idle/prepare, poll, check (setImmediate) и close. Между каждой фазой сначала дренируются колбэки process.nextTick (до микрозадач), что делает его примитивом с наивысшим асинхронным приоритетом в Node и риском голодания при рекурсивном использовании. setImmediate срабатывает в фазе check текущей итерации; setTimeout(0) — в фазе timers следующей итерации: внутри I/O-колбэков setImmediate всегда выигрывает; вне них порядок недетерминирован. Поскольку у Node нет шага рендеринга или событий ввода, эквивалент INP браузера — задержка event loop, измеряемая с perf_hooks.monitorEventLoopDelay(). Всплеск задержки p99 означает, что обработчик удерживает единственный главный поток — блокируя каждый параллельный запрос — и лечение идентично браузеру: более короткие задачи, стриминговые парсеры или Worker threads для работы, зависящей от CPU.

Связанные уроки
встречается в267
Продолжить восхождение ↑React, Vue и наблюдаемость INP в продакшене
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.