Engineering

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

Jan Tůma
Jan Tůma 6 min čtení

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í perf_hooks
  • 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 setImmediate() 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: postMessage() (kopíruje data) nebo SharedArrayBuffer (sdílená paměť)
  • Atomics 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:

  • postMessage() - jednodušší, data se kopírují (structured clone), vhodné pro menší zprávy
  • SharedArrayBuffer - sdílená paměť bez kopírování, vyžaduje Atomics pro synchronizaci, vhodné pro velké datasety nebo časté komunikace
// 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

Diagram implementace event loop – libuv, deno_core a vlastní implementace dle HTML5 specifikace

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

Základní pojmy: Macrotasks vs Microtasks

Macrotasks

  • setTimeout
  • setInterval
  • setImmediate
  • I/O operace

Microtasks (vyšší priorita)

  • Promises (.then, .catch, .finally)
  • queueMicrotask

nextTick queue (nejvyšší priorita)

  • process.nextTick - 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

Diagram fází event loop

Poznámky k exit fázi:

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

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í. Fáze zůstávají stejné (timers v timers fázi, setImmediate v check fázi). Změnilo se, kdy se timers kontrolují, zda už vypršely.

Před Node.js 20:

Zpracování timerů v event loop před Node.js 20

Od Node.js 20:

Zpracování timerů v event loop od Node.js 20

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

Monitoring event loop

APM nástroje sledují event loop lag automaticky:

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

Pro ruční měření: perf_hooks.monitorEventLoopDelay()

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: start, end, pak buď timeout nebo immediate, timeout - 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();

start a pak… nic. Nekonečný loop! await Promise.resolve() 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 SharedArrayBuffer
  • Pozor na starvation - process.nextTick a nekonečné async smyčky mohou zablokovat vše ostatní

Užitečné odkazy

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.

Jan Tůma
Jan Tůma
BE Techlead, Cookielab

Cookielabber. Píše kód, pije kafe, dodává software.

Máte projekt, který potřebuje tuto úroveň péče?

Promluvte si přímo s founderem. Nejdřív posloucháme, radíme upřímně a stavíme jen když to dává smysl.

Cookielab zakladatelé — Radek Míka, Martin Homolka, Jakub Kohout

Pojďme probrat váš byznys...

nebo

...vaši kariéru.

Otevřené pozice