Menu

Node.js Event Loop: Phasen, nextTick und setImmediate

Die Node.js Event Loop führt deine Callbacks in Phasen aus (timers, poll, check, close), und zwischen zwei Callbacks leert sie zuerst die process.nextTick-Queue und dann die Promise-Queue. Führ die Beispiele aus und sieh dir die genaue Reihenfolge an.

Diese Seite enthält ausführbare Editoren. Bearbeiten, ausführen und Ausgabe sofort sehen.

Die Node.js Event Loop ist die Schleife, die entscheidet, welcher Callback als Nächstes läuft, sobald dein synchroner Code fertig ist. Sie durchläuft Phasen in einem festen Zyklus: timers (setTimeout, setInterval), pending callbacks, poll (I/O wie gelesene Dateien und Netzwerkdaten), check (setImmediate) und close callbacks. Nach jedem Callback leert Node zuerst die process.nextTick-Queue und dann die Promise-Queue, deshalb laufen diese immer, bevor die Loop weitermacht.

Es gibt sync, nextTick, promise, setTimeout aus, also genau umgekehrt zur Reihenfolge im Code. Synchroner Code ist immer zuerst fertig, dann leert Node die nextTick-Queue, dann die Promise-Queue, und erst danach startet die Loop und erreicht die timers-Phase.

Die Phasen der Event Loop

Die Loop von Node stammt aus libuv, der C-Bibliothek, die Node für I/O verwendet. Vor der ersten Runde führt sie die Timer aus, die schon fällig sind, und danach durchläuft jede Runde diese Phasen der Reihe nach:

  1. Pending callbacks: einige System-Callbacks, die aus der vorigen Runde verschoben wurden, etwa manche Netzwerkfehler.
  2. Poll: neue I/O-Ereignisse und ihre Callbacks (ein Dateilesevorgang ist fertig, Daten sind an einem Socket angekommen). Wenn sonst nichts geplant ist, wartet die Loop hier.
  3. Check: setImmediate-Callbacks.
  4. Close callbacks: 'close'-Events, etwa ein Socket, der zerstört wurde.
  5. Timers: Callbacks von setTimeout und setInterval, deren Zeit gekommen ist.

Viele Anleitungen nennen die Timer in jeder Runde zuerst, so hat Node vor Version 20 (libuv 1.45) gearbeitet. Die Callbacks laufen so oder so in derselben Reihenfolge: Auf die Timer einer Runde folgt die poll-Phase der nächsten Runde.

Zwei Queues liegen außerhalb der Phasen: die nextTick-Queue und die Microtask-Queue (Promise-Callbacks und queueMicrotask). Node leert beide nach jedem einzelnen Callback, in jeder Phase, nextTick zuerst. Die Loop dreht sich weiter, solange es etwas gibt, worauf sie warten muss (ein Timer, eine offene Datei oder ein Socket, ein Server), und der Prozess endet, wenn nichts mehr übrig ist.

Das ist die eigene Loop von Node. Der Browser teilt die Idee von Tasks und Microtasks, hat aber keine Phasen, kein setImmediate und kein process.nextTick; die Seite zur JavaScript Event Loop zeigt das Bild auf Sprachebene.

process.nextTick vs. Promises

Beide laufen, bevor die Loop weitermacht, aber sie werden nicht gemischt. Node arbeitet die ganze nextTick-Queue ab, auch Ticks, die währenddessen hinzukommen, und erst dann die Promise-Queue. Sag dieses Beispiel vorher:

Die Reihenfolge ist tick 1, tick 2, tick from tick, promise 1, promise 2. Der Tick, der innerhalb eines Ticks geplant wird, landet in derselben Queue und läuft trotzdem vor jedem Promise, obwohl das Promise zuerst erstellt wurde.

Auf oberster Ebene eines ES-Moduls (einer Datei mit import) tauschen die beiden die Plätze: Der Code des Moduls läuft selbst in einem Promise-Job, deshalb sind seine Promise-Callbacks fertig, bevor Node zur nextTick-Queue kommt. Innerhalb von Callbacks gilt in beiden Modulsystemen die Regel, dass nextTick zuerst kommt.

setImmediate vs. setTimeout(fn, 0)

setTimeout(fn, 0) bedeutet eigentlich "nach mindestens 1 ms, in der timers-Phase". setImmediate(fn) bedeutet "in der nächsten check-Phase". Was zuerst läuft, hängt davon ab, wo du sie aufrufst.

Innerhalb eines I/O-Callbacks ist die Reihenfolge fest. Der Callback läuft in der poll-Phase, die check-Phase folgt direkt danach, und die timers-Phase kommt erst danach:

Es gibt jedes Mal nextTick, setImmediate, setTimeout aus. Damit ist setImmediate die richtige Wahl für "führ das nach der aktuellen I/O-Arbeit aus".

Im Hauptmodul, außerhalb jedes Callbacks, ist die Reihenfolge der beiden nicht festgelegt:

// Order depends on how fast the process started
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));

Wenn beim Start der Loop mindestens 1 ms vergangen ist, ist der Timer fällig und wird zuerst ausgegeben; sonst gewinnt setImmediate. Schreib keinen Code, der von einem der beiden Ergebnisse abhängt.

LäuftWofür
process.nextTick(fn)Direkt nach der aktuellen Operation, vor PromisesEin Event oder einen Fehler auslösen, nachdem der Aufrufer Listener angehängt hat
Promise.then, queueMicrotaskWenn die nextTick-Queue leer istNormaler asynchroner Code
setImmediate(fn)check-Phase, nach I/O-CallbacksDer Loop zwischen Arbeitsabschnitten eine Runde lassen
setTimeout(fn, 0)timers-Phase, mindestens 1 ms späterVerzögerungen, bei denen der genaue Zeitpunkt egal ist

Die Event Loop blockieren

Weil es nur einen JavaScript-Thread gibt, hält ein Callback, der lange läuft, alles andere auf: Timer, I/O-Callbacks und auf einem Server jede andere Anfrage. Die Verzögerung eines Timers ist ein Minimum, keine Garantie.

Zuerst erscheint loop finished, dann true: Der Timer war nach 10 ms fällig, konnte aber erst laufen, als die Busy-Loop den Thread zurückgab. Auf einem Server friert dieselbe Schleife in einem Route-Handler jede andere Anfrage für eine halbe Sekunde ein.

Die Lösung für lange Arbeit: Teil sie auf und gib der Loop zwischen den Teilen mit setImmediate eine Runde:

Der Timer kommt nach Teil 1 an die Reihe. Ersetz setImmediate(() => work(part + 1)) durch work(part + 1) und führ es noch einmal aus: Alle drei Teile laufen am Stück, und timer erscheint zuletzt.

Für Arbeit, die durchgehend schwer ist, etwa Bildverarbeitung, reicht Aufteilen nicht: Verschieb sie mit worker_threads weg vom Haupt-Thread.

Häufige Fehler

process.nextTick rekursiv aufrufen. Ein nextTick-Callback, der immer wieder einen neuen nextTick plant, lässt die Loop nie weiterlaufen, und Timer und I/O verhungern. Nimm setImmediate für wiederholte Arbeit.

Synchrone APIs in einem Server verwenden. fs.readFileSync, crypto.pbkdf2Sync und große JSON.parse-Aufrufe blockieren die Loop für alle Nutzer. Nimm in Request-Handlern die asynchronen Varianten.

Erwarten, dass setTimeout(fn, 0) sofort läuft. Es läuft nach dem gesamten synchronen Code, jedem nextTick und jedem Promise-Callback, und erst, wenn die Loop die timers-Phase erreicht.

Häufig gestellte Fragen

Ist Node.js single-threaded?

Dein JavaScript läuft in einem Thread, zwei Callbacks laufen also nie gleichzeitig. Node selbst nutzt mehr Threads: libuv hält einen Pool (standardmäßig vier Threads) für Dateisystem-Arbeit, DNS-Lookups und einen Teil von crypto bereit und gibt die Ergebnisse an die Loop zurück.

Wann beendet sich ein Node.js-Programm?

Wenn die Event Loop auf nichts mehr warten muss: keine Timer, kein ausstehendes I/O, keine offenen Server oder Sockets. Ein laufendes setInterval oder ein Server, der lauscht, hält den Prozess am Leben, bis du ihn löschst oder schließt.

Ist die Event Loop von Node.js dieselbe wie im Browser?

Die Idee ist dieselbe, ein Thread arbeitet eingereihte Callbacks ab, aber die Details unterscheiden sich. Der Browser hat kein setImmediate und kein process.nextTick und rendert zwischen den Tasks, während Node die Phasen von libuv durchläuft. Die Seite zur JavaScript Event Loop behandelt den gemeinsamen Teil auf Sprachebene.

Woran erkenne ich, dass die Event Loop blockiert ist?

Miss, wie spät sie läuft: monitorEventLoopDelay() aus perf_hooks zeichnet die Verzögerung auf, und ein Mittelwert im zweistelligen Millisekundenbereich heißt, dass ein Callback den Thread zu lange hält. Starte Node mit --cpu-prof, um zu sehen, welche Funktion es ist.

Illustration der Programmiersprachen bei Coddy

Lerne mit Coddy zu programmieren

Los geht's