Skip to main content

У чому різниця між process.nextTick() та setImmediate()?

process.nextTick() ставить колбек у чергу, яка виконується одразу після того як поточний стек викликів спустошується, але до того як цикл подій переходить до наступної фази. setImmediate() ставить колбек у чергу перевірки (check phase), яка запускається після фази опитування I/O.

Теорія

TL;DR

  • Цикл подій у Node.js має фази: timers, poll (I/O), check, close. nextTick спрацьовує між будь-якими двома фазами; setImmediate тільки у фазі check.
  • process.nextTick() використовує мікротаск-чергу, яку V8 повністю дренує до того як libuv переходить до наступної фази. setImmediate() реєструє обробник uv_check_t у libuv.
  • Рекурсивний nextTick морить голодом I/O. Рекурсивний setImmediate ні.
  • Правило вибору: треба виконати до будь-якого I/O? nextTick. Треба поступитися I/O і таймерам? setImmediate.
  • Обидва спрацьовують після синхронного коду. Порядок між ними передбачуваний тільки всередині I/O-колбека.

Швидкий приклад

js
setImmediate(() => console.log('I - setImmediate')); process.nextTick(() => console.log('N - nextTick')); Promise.resolve().then(() => console.log('P - Promise')); console.log('sync'); // Вивід (завжди): // sync // N - nextTick // P - Promise // I - setImmediate

nextTick спрацьовує першим, бо V8 дренує мікротаск-чергу до того як цикл подій рухається далі. Promise.then() у тій самій черзі, але після nextTick-колбеків. setImmediate чекає фазу check.

Головна різниця

Різниця між nextTick і setImmediate не просто у часі запуску, а в тому, де в рушії живе колбек. Колбеки nextTick потрапляють у спеціальну чергу, яку Node дренує синхронно всередині InternalCallbackScope, до того як uv_run() взагалі щось робить. setImmediate реєструє обробник uv_check_t у libuv, і той спрацьовує тільки коли цикл подій досягає фази check після опитування I/O. Саме тому рекурсивний nextTick може повністю заблокувати I/O, а setImmediate ні.

Коли що використовувати

  • Виконати до будь-якого I/O в поточній ітерації: process.nextTick() (наприклад, emit події після конструктора, щоб слухачі встигли підключитися).
  • Поступитися I/O і таймерам: setImmediate() (наприклад, відкласти очищення після res.json() в Express).
  • Рекурсивний опит або retry-цикли: тільки setImmediate. Рекурсія nextTick глибиною 1k+ блокує таймери на 100мс і більше.
  • Узгодженість з Promise.then(): nextTick спрацьовує до Promise-колбеків у тому самому циклі дренування мікротасків.

Пріоритет у циклі подій

ПріоритетМеханізмЧерга / фаза
1 (найвищий)process.nextTick()Мікротаск (до фаз)
2Promise.then()Мікротаск (до фаз)
3setTimeout(fn, 0)Фаза timers
4setImmediate()Фаза check

Як Node обробляє їх зсередини

Node.js використовує libuv для циклу подій із фазами у такому порядку: timers, poll (I/O), check, close. setImmediate() реєструє обробник uv_check_t, який libuv викликає у uv__run_check() після uv__io_poll(). process.nextTick() обходить libuv повністю: деструктор InternalCallbackScope у Node скидає nextTick-чергу через MicrotaskQueue::PerformCheckpointInternal() у V8 після кожного скрипту або колбека, але до того як uv_run() переходить до наступної фази. Тому в документації Node.js написано, що nextTick технічно не є частиною циклу подій.

Особисте спостереження з продакшену: загортати process.exit() у nextTick після I/O-колбека здається нешкідливим, але це може обрізати інші незавершені I/O-операції. setImmediate в цьому паттерні безпечніший.

Типові помилки

Помилка 1: nextTick для будь-якого асинхронного відкладення

js
fs.readFile('file.txt', (err, data) => { process.nextTick(() => process.exit(0)); // виходить до завершення інших I/O });

nextTick спрацьовує до того як відновлюється фаза poll, тому інші незавершені fs-колбеки ніколи не виконаються. Тут потрібен setImmediate.

Помилка 2: рекурсивний nextTick у логіці опитування

js
function poll() { process.nextTick(poll); // ніколи не поступається циклу подій } poll();

Це морить голодом весь I/O і таймери. При глибині 1M+ цикл зависає безповоротно. Правильно: setImmediate(poll).

Помилка 3: очікування що вкладений setImmediate виконається до зовнішнього

js
process.nextTick(() => { setImmediate(() => console.log('вкладений setImmediate')); }); setImmediate(() => console.log('зовнішній setImmediate')); // Вивід: // зовнішній setImmediate // вкладений setImmediate

setImmediate, поставлений у черзі всередині nextTick-колбека, пропускає поточну фазу check і виконується в наступному циклі. Це дивує тих, хто очікує зворотного порядку.

Помилка 4: голодування у рекурсивній обробці

js
setImmediate(() => console.log('I - setImmediate')); // поставлено в чергу let depth = 0; function recurse() { if (++depth > 5) return console.log('Done'); process.nextTick(recurse); } recurse(); // Вивід: // Done // I - setImmediate (затримано усіма 6 nextTick-викликами)

Шість викликів nextTick вже помітно затримують setImmediate. При реальних масштабах (глибина 1k+) ти блокуєш таймери на 100мс і більше, що ламає код чутливий до таймаутів.

Де зустрічається у реальних проектах

  • Express route-обробники: nextTick для звільнення з'єднання з базою після res.json(), до відновлення фази poll.
  • Паттерн EventEmitter: відкладання this.emit('ready') у конструкторі через nextTick, щоб слухачі встигли підключитися синхронно.
  • Hapi auth-плагіни: setImmediate після обробки запиту, щоб не блокувати мережеві колбеки.
  • async_hooks: nextTick виконується в поточному async-контексті без затримки фази, корисно для before/after-хуків інструментації.
  • PM2 кластеризація: setImmediate для відкладання міжпроцесних повідомлень, щоб I/O воркерів не голодував.

Питання на співбесіді

Q: Чи може setImmediate виконатись раніше за process.nextTick()?
A: У Node.js >= 11 при виклику обох з верхнього рівня модуля ні. У старіших версіях або в REPL з активною фазою timers порядок міг бути непередбачуваним. У сучасному Node мікротаск-черга завжди дренується до будь-якої фази.

Q: Як nextTick і Promise.then() співвідносяться між собою?
A: Обидва у мікротаск-черзі, яка дренується до переходу між фазами. nextTick-колбеки дренуються першими, потім Promise-колбеки. Тому process.nextTick(cb) виконується до Promise.resolve().then(cb) якщо обидва поставлені в одній ітерації.

Q: Що станеться якщо викликати nextTick всередині setImmediate-колбека?
A: nextTick-колбек виконається одразу після завершення setImmediate-колбека, до того як фаза check перейде до наступного setImmediate у черзі. Він не чекає наступної ітерації циклу подій.

Q: (Senior) Що в libuv обробляє setImmediate, а що у V8 обробляє nextTick? Опиши порядок виконання.
A: setImmediate реєструє uv_check_t-дескриптор. libuv викликає їх у uv__run_check() всередині uv_run(), після uv__io_poll(). nextTick не потрапляє в libuv взагалі: деструктор InternalCallbackScope у Node скидає nextTick-чергу через MicrotaskQueue::PerformCheckpointInternal() у V8 після кожної C++-межі колбека. Тобто nextTick спрацьовує в проміжку між будь-якими двома libuv-колбеками, а не у іменованій фазі.

Q: Як діагностувати голодування від nextTick у продакшені?
A: Використай clinic.js doctor або запусти Node з --trace-event-categories v8. Clinic показує затримку циклу подій у часі. Якщо затримка циклу зростає при низькому CPU, рекурсивний nextTick є найпоширенішою причиною. Додай лічильник і після N ітерацій переключись на setImmediate.

Приклади

Базовий: порядок виконання поряд

js
console.log('1'); process.nextTick(() => console.log('2 - nextTick')); Promise.resolve().then(() => console.log('3 - Promise')); setImmediate(() => console.log('4 - setImmediate')); setTimeout(() => console.log('5 - setTimeout'), 0); console.log('6'); // Вивід (Node 18): // 1 // 6 // 2 - nextTick // 3 - Promise // 4 - setImmediate (або 5 перед 4 для setTimeout - порядок між ними поза I/O не гарантований)

nextTick і Promise дренують мікротаск-чергу синхронно до будь-якої фази циклу. setImmediate і setTimeout(fn, 0) обидва у фазах циклу, і їхній відносний порядок поза I/O-колбеком не гарантований.

Середній: паттерн конструктора EventEmitter

js
const EventEmitter = require('events'); class MyEmitter extends EventEmitter { constructor() { super(); // nextTick відкладає emit до завершення конструктора, // даючи час підключити слухача через .on() до того як подія спрацює process.nextTick(() => { this.emit('ready'); }); } } const emitter = new MyEmitter(); emitter.on('ready', () => console.log('готово!')); // Вивід: готово! // Без nextTick 'ready' спрацює під час конструктора і слухач ще не підключений

Це один із найпоширеніших законних використань nextTick у бібліотечному коді. setImmediate теж спрацює, але nextTick гарантує що emit відбудеться до будь-якого I/O в тій самій ітерації.

Старший рівень: демонстрація голодування

js
setImmediate(() => console.log('setImmediate - має виконатись незабаром')); let depth = 0; function recurse() { if (++depth > 5) return console.log(`Done at depth ${depth}`); process.nextTick(recurse); } recurse(); // Вивід: // Done at depth 6 // setImmediate - має виконатись незабаром <-- затримано усіма 6 nextTick-викликами // При depth 1_000_000 setImmediate і будь-які таймери // були б заблоковані на сотні мілісекунд. // Виправлення: замінити process.nextTick(recurse) на setImmediate(recurse)

setImmediate-колбек, поставлений у чергу до recurse(), не виконується поки вся nextTick-ланцюжок не завершиться. У реальному сценарії з логером або health-check, що використовує рекурсивний nextTick, саме так непомітно ламаються операції чутливі до таймаутів.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?