Node.js Event Loop: Praktický přehled a best practices

Event Loop je základní mechanismus, který umožňuje Node.js provádět asynchronní operace. Jde o nekonečnou smyčku, která zpracovává úlohy z fronty a běží tak dlouho, dokud jsou v ní nějaké naplánované tasks.

Proč je to důležité:

  • Umožňuje concurrency (souběžnost) bez nutnosti více vláken
  • Výkon aplikace závisí na tom, jak rychle loop zpracovává tasks - měříme pomocí <inline-code>perf_hooks<inline-code>
  • Alternativy v jiných jazycích: více vláken, goroutines (Go), Virtual Threads (Java 21+)

Klíčové vlastnosti a best practices

Jeden sdílený thread
Node.js vykonává uživatelský kód v jednom vlákně. Blokující operace (filesystem, kryptografie) běží v podpůrných vláknech (thread pool).

Run-to-completion
Kód vždy doběhne do konce - díky tomu nevznikají synchronizační problémy typické pro multithreading.

Event Loop Starvation
Zahlcení event loop příliš mnoha asynchronními operacemi - loop nestíhá zpracovávat.

Zablokování
Náročné výpočty v hlavním vlákně zablokují celý event loop.
Jak se bránit zablokování
Chunking - rozdělení dlouhých operací na menší části s <inline-code>setImmediate()<inline-code> mezi nimi
Worker Threads - paralelní JS execution v rámci jednoho procesu (stabilní od Node.js v12 LTS)
Každý worker má vlastní event loop a V8 instance
Komunikace: <inline-code>postMessage()<inline-code> (kopíruje data) nebo <inline-code>SharedArrayBuffer<inline-code> (sdílená paměť)
<inline-code>Atomics<inline-code> pro synchronizaci při sdílené paměti
Child Process / Cluster - oddělené procesy (vyšší izolace, ale větší overhead)
Proces má vlastní alokovanou paměť, vlákno sdílí paměť s rodičovským procesem
Proto je vytvoření procesu náročnější než vlákna

Komunikace s workers:
<inline-code>postMessage()<inline-code> - jednodušší, data se kopírují (structured clone), vhodné pro menší zprávy
<inline-code>SharedArrayBuffer<inline-code> - sdílená paměť bez kopírování, vyžaduje <inline-code>Atomics<inline-code> pro synchronizaci, vhodné pro velké datasety nebo častou komunikaci

// postMessage - jednodušší, kopíruje data
worker.postMessage({ type: 'process', data: myArray });
worker.on('message', (result) => console.log(result));

// SharedArrayBuffer - sdílená paměť, bez kopírování
const shared = new SharedArrayBuffer(1024);
const arr = new Int32Array(shared);
worker.postMessage({ buffer: shared });
// Worker může přímo číst/zapisovat do arr
// Atomics.add(arr, 0, 1) - thread-safe increment

Implementace

Event loop není součástí JS enginu (V8/JSC), ale je implementován externě.

Základní pojmy: Macrotasks vs Microtasks

Macrotasks

  • <inline-code>setTimeout<inline-code>
  • <inline-code>setInterval<inline-code>
  • <inline-code>setImmediate<inline-code>
  • I/O operace

Microtasks (vyšší priorita)

  • Promises (<inline-code>.then<inline-code>, <inline-code>.catch<inline-code>, <inline-code>.finally<inline-code>)
  • <inline-code>queueMicrotask<inline-code>

nextTick queue (nejvyšší priorita)

  • <inline-code>process.nextTick<inline-code> - vlastní oddělená fronta, není to microtask
  • Zpracovává se PŘED microtask queue

Klíčové pravidlo: Mezi fázemi event loop se nejprve vyprázdní nextTick queue, pak microtask queue, a teprve potom se pokračuje další fází.

macrotask₁ → nextTick queue → microtask queue → macrotask₂ → ...
Toto platí i mezi fázemi event loop - obě fronty mají vždy přednost před macrotasks.
Promise se dostane do event loop až v momentě resolve/reject, ne když je pending.

Fáze Event Loop


Poznámky k exit fázi:

  • <inline-code>beforeExit<inline-code> - poslední šance naplánovat práci → pokud ano, loop pokračuje od timers
  • <inline-code>exit<inline-code> - pouze synchronní kód, async operace se ignorují
  • Explicitní <inline-code>process.exit()<inline-code> přeskočí <inline-code>beforeExit<inline-code>

nextTick a Microtasks se zpracovávají:

  • Mezi fázemi event loop
  • Po každém callbacku v rámci fáze (od Node.js 11+)

Obě fronty (nextTick → microtasks) se vždy kompletně vyprázdní před pokračováním

Důležité detaily:

  • Kód se spouští PŘED vstupem do loop
  • V každé fázi loop zpracuje všechny tasks (nebo do hard limitu)
  • Nový macrotask stejného typu nejde do aktuální iterace
  • Poll fáze může blokovat, pokud čeká na I/O a není nic dalšího naplánovaného
  • Loop se ukončí, když nejsou žádné aktivní handles, pending requests ani čekající timers

Změna v Node.js 20+ (libuv 1.45.0)
⚠️ Breaking change: Toto může ovlivnit timing aplikací
Co se změnilo: Fáze zůstávají stejné (timers v timers fázi, <inline-code>setImmediate<inline-code> v check fázi). Změnilo se, kdy se timers kontrolují, zda už vypršely.

Před Node.js 20:

Od Node.js 20:

Důsledek: Pod zátěží (zahlcená poll fáze) mohou timers čekat déle než dříve. Řešení: nepoužívat <inline-code>setTimeout(fn, 0)<inline-code> pro time-sensitive operace, monitorovat event loop.

Monitoring event loop
APM nástroje sledují event loop lag automaticky:

  • OpenTelemetry (<inline-code>@opentelemetry/instrumentation-runtime-node<inline-code>)
  • Datadog, New Relic, Dynatrace
  • Clinic.js pro lokální diagnostiku

Pro ruční měření: <inline-code>perf_hooks.monitorEventLoopDelay()<inline-code>

Praktické příklady (Challenge)

Příklad 1: setTimeout vs setImmediate

console.log('start');
const immediate = setImmediate(() => console.log('immediate'));
setTimeout(() => {
  console.log('timeout');
  clearImmediate(immediate);
}, 0);
console.log('end');

Výstup

<inline-code>start<inline-code>, <inline-code>end<inline-code>, pak buď <inline-code>timeout<inline-code> nebo <inline-code>immediate, timeout<inline-code> - záleží na tom, zda je timeout připraven při vstupu do loop.

Příklad 2: Event Loop Starvation

async function main() {
  console.log('start');
  let run = true;
  setTimeout(() => {
    console.log('timeout');
    run = false;
  }, 0);
  while (run) {
    await Promise.resolve();
  }
}
main();


Výstup

<inline-code>start<inline-code> a pak... nic. Nekonečný loop!

<inline-code>await Promise.resolve()<inline-code> je microtask. Microtasks mají vyšší prioritu a zpracovávají se kompletně po každém macrotask. Timeout (macrotask) se nikdy nedostane na řadu, protože while loop neustále přidává nové microtasks.

Shrnutí key takeaways

Event loop = srdce Node.js - pochopení je klíčové pro psaní výkonného kódu
Run-to-completion - callback vždy doběhne bez přerušení, žádné race conditions uvnitř
nextTick > Microtasks > Macrotasks - nextTick queue se zpracuje první, pak promises
Neblokuj hlavní vlákno - dlouhé výpočty přesuň do worker threads
Worker threads efektivně - používej worker pool (ne per-task), počet ≈ CPU jader, pro velká data <inline-code>SharedArrayBuffer<inline-code>
Pozor na starvation - <inline-code>process.nextTick<inline-code> a nekonečné async smyčky mohou zablokovat vše ostatní

Užitečné odkazy

libuv dokumentace
Node.js Event Loop docs
Clinic.js - diagnostika výkonu Node.js
HTML5 Event Loop spec
Bun runtime
Deno runtime

P.S. Pokud provozujete Node.js v produkci a chcete se vyhnout nepříjemným výkonovým překvapením, rádi vám pomůžeme. Ozvěte se nám, pokud chcete druhý názor nebo jít do hlubší diskuze.

Node.js Event Loop: Praktický přehled a best practices
Jan Tůma
BE Techlead
By clicking “Accept”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts. View our Privacy Policy for more information.