У чому різниця між 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-колбека.
Швидкий приклад
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 - setImmediatenextTick спрацьовує першим, бо 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() | Мікротаск (до фаз) |
| 2 | Promise.then() | Мікротаск (до фаз) |
| 3 | setTimeout(fn, 0) | Фаза timers |
| 4 | setImmediate() | Фаза 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 для будь-якого асинхронного відкладення
fs.readFile('file.txt', (err, data) => {
process.nextTick(() => process.exit(0)); // виходить до завершення інших I/O
});nextTick спрацьовує до того як відновлюється фаза poll, тому інші незавершені fs-колбеки ніколи не виконаються. Тут потрібен setImmediate.
Помилка 2: рекурсивний nextTick у логіці опитування
function poll() {
process.nextTick(poll); // ніколи не поступається циклу подій
}
poll();Це морить голодом весь I/O і таймери. При глибині 1M+ цикл зависає безповоротно. Правильно: setImmediate(poll).
Помилка 3: очікування що вкладений setImmediate виконається до зовнішнього
process.nextTick(() => {
setImmediate(() => console.log('вкладений setImmediate'));
});
setImmediate(() => console.log('зовнішній setImmediate'));
// Вивід:
// зовнішній setImmediate
// вкладений setImmediatesetImmediate, поставлений у черзі всередині nextTick-колбека, пропускає поточну фазу check і виконується в наступному циклі. Це дивує тих, хто очікує зворотного порядку.
Помилка 4: голодування у рекурсивній обробці
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.
Приклади
Базовий: порядок виконання поряд
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
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 в тій самій ітерації.
Старший рівень: демонстрація голодування
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, саме так непомітно ламаються операції чутливі до таймаутів.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.