Van az a Node-hiba, amelyben a CPU nem pörög, a processz sem halt meg, mégis késik a timer, az I/O callback és vele együtt egy-egy HTTP-válasz. A stack trace többnyire semmitmondó. A nyom végén gyakran egy ártatlannak látszó process.nextTick() van: egy helper, amely „csak aszinkronra teszi” a callbacket. Csakhogy ez nem egy átlagos aszinkron sor. A Node saját VIP-sávot tart fenn neki.

A Node jelenlegi API-dokumentációja a process.nextTick()-et már Legacy státuszúnak jelöli, és a legtöbb új alkalmazáskódhoz a queueMicrotask()-ot ajánlja. Nem azért, mert holnap eltűnik, hanem mert a név sokkal szelídebb viselkedést ígér, mint amit kapunk.
Nem a „következő körben” fut
A nextTick callbackje a jelenlegi JavaScript-hívási verem kiürülése után fut le, mielőtt a Node továbbengedné az eseményhurkot. A callbackeket a Node saját next tick queue-jába teszi, és ezt teljesen kiüríti. Csak azután ürül a V8 mikrotask-sora, ahol a Promise-ok then/catch/finally kezelői és a queueMicrotask() várnak. Ezt a két külön sort a globális API dokumentációja is külön nevezi meg.
Ez nem ugyanaz, mint a böngészős „majd rögtön” intuíció. A mikrofeladatok amúgy is elsőbbséget élveznek a következő normál task előtt; a mikrotask-modell ráadásul addig dolgozza a sort, amíg az üres nem lesz. Node-ban erre jön rá még egy, eléjük vágó sor. A nextTick tehát nem az event loop egyik fázisa, hanem a belépőnél álló biztonsági őr, aki mindenkit előreenged a listáról.
Promise.resolve().then(() => console.log('promise'));
queueMicrotask(() => console.log('microtask'));
process.nextTick(() => console.log('nextTick'));
setImmediate(() => console.log('immediate'));
// CommonJS: nextTick, promise, microtask, immediate
Ez a minta CommonJS-fájlként pontosan ezt írta ki Node 22-n. A setImmediate csak később kap esélyt, mert az már ténylegesen visszaadja a vezérlést az eseményhuroknak. A Node event loop útmutatója ezért nem kezeli a nextTick-et normál fázisként.
A meglepetés: ESM-ben más lesz a kimenet
Ugyanazt a négy sort .mjs-ként futtatva nálam ez jött: promise, microtask, nextTick, immediate. Na, ezt nem tudtam: az ESM modul kiértékelése maga is a mikrotask-sor része, ezért ott a már beütemezett Promise- és queueMicrotask-callbackek előbb futnak le. A hivatalos összehasonlítás külön leírja a CJS–ESM sorrendkülönbséget.
Ez jó ok arra, hogy könyvtárban ne építsünk publikus viselkedést arra, hogy „a nextTick biztosan minden Promise előtt jön”. A csomag fogyasztója lehet ESM-es, CommonJS-es, vagy a bundlere át is rendezheti a világot. Egy callback sorrendje ritkán olyan üzleti követelmény, amelyet érdemes egy modulformátumhoz kötni.
A valódi hiba: nem engeded levegőhöz jutni az I/O-t
A baj akkor lesz éles, ha egy nextTick-callback újabb nextTick-et tesz sorba. A Node előbb mindig kiüríti ezt a sort; ha közben újratöltjük, timer, socket, fájlolvasás és beérkező kérés nem jut el a JavaScriptig. Az API-dokumentáció egyenesen figyelmeztet a rekurzív hívásból létrejövő végtelen ciklusra. Kéréskezelőben ez különösen kellemetlen módja annak, hogy az egész processz egyszerre „lassú legyen”.
Fontos korrekció: a queueMicrotask()-ra csere nem CPU-yield. Rekurzívan sorba rakott mikrotaskok ugyanúgy nem engedik a következő taskot futni. Ha a cél a nagy, szinkron munka darabolása, olyan pontra kell átadni a vezérlést, amely tényleg visszatér az event loophoz:
import { setImmediate } from 'node:timers/promises';
let completed = 0;
setTimeout(() => console.log(`timer: ${completed}`), 0);
for (let batch = 0; batch < 3; batch += 1) {
for (let i = 0; i < 100_000; i += 1) completed += 1;
await setImmediate(); // itt az I/O is kap egy kört
}
console.log(`done: ${completed}`);
A Promise-alapú setImmediate() erre egy olvasható eszköz. Nem teszi párhuzamossá a CPU-munkát, csak alkalmat ad I/O-nak és más callbackeknek a futásra. Ha egy batch is érezhetően hosszú, a darabolás kozmetika: akkor Worker Thread vagy külön worker process a korrekt irány.
Mit használjak helyette?
- Konzisztensen késleltetett callbackhez — például egy cache-hit és egy aszinkron betöltés azonos API-viselkedéséhez —
queueMicrotask()az alapértelmezett, platformok között is hordozható választás. - Promise-kezelők elé szándékosan befurakodni — ritka, de Node-specifikus kompatibilitási eset — maradhat a
process.nextTick(). Legyen korlátos, és a sorrend legyen dokumentált. - Fair ütemezéshez vagy CPU-daraboláshoz ne mikrotaskot használj, hanem
setImmediate()-et; komoly számításhoz pedig ne az API-szerver event loopja végezze a nehézemelést.
Én production kódban új helyre már nem írnám be rutinból a process.nextTick()-et. Nem tiltandó relikvia, inkább nagyon éles szerszám: egy kis callback-sorrendet lehet vele szépen megoldani, de egy rossz rekurzióval minden kapcsolatot az ajtó előtt felejtünk.