Node.js: principy fungování a architektura event loopu

Node.js: Principy fungování a event-loop architektura

Co je Node.js a proč event loop

Node.js je běhové prostředí pro JavaScript mimo prohlížeč, postavené na V8 (JIT kompilátoru od Googlu) a nativní knihovně libuv pro asynchronní I/O. Klíčovým principem je jednovláknová smyčka událostí, která multiplexuje tisíce I/O operací bez blokování vlákna JavaScriptu. Výsledkem je vysoká propustnost při nízké latenci u služeb náročných na I/O (API, proxy, realtime push) při zachování jednoduchého programovacího modelu.

Stavební kameny: V8, libuv, bindings a standardní knihovna

  • V8: parsuje JS, optimalizuje a JIT kompiluje horké úseky kódu, spravuje garbage collector (GC) a haldu.
  • libuv: poskytuje smyčku událostí, neblokující síťové I/O, časovače, práci se souborovým systémem přes fond vláken, signály a IPC.
  • Bindings: propojení mezi JavaScriptem a vrstvou C/C++ (N-API, node-addon-api) pro volání nativních funkcí.
  • Standardní knihovna Node.js: moduly jako fs, net, http/https, crypto, stream, worker_threads, postavené nad libuv a V8.

Event loop: fáze a pořadí zpracování

Event loop v Node.js je logická smyčka řízená knihovnou libuv, která postupně prochází jednotlivými fázemi. Každá fáze má frontu úloh (makroúloh). Zjednodušené pořadí:

  1. Timers: zpracování setTimeout/setInterval, jejichž časový limit vypršel.
  2. Pending Callbacks: nízkoúrovňová systémová zpětná volání (např. některé chyby TCP).
  3. Idle/Prepare: interní fáze knihovny libuv.
  4. Poll: hlavní část – přijímá I/O události, obsluhuje sockety, čte a zapisuje data; pokud je fronta prázdná, může čekat na příchod události (až na výjimky).
  5. Check: provádějí se callbacky setImmediate.
  6. Close Callbacks: například události 'close' u socketů a handlerů.

Mezi jednotlivými fázemi se vždy vyprazdňuje fronta mikroúloh (microtasks), tedy úlohy process.nextTick a pokračování Promise .then()/await.

Mikroúlohy vs. makroúlohy: jemná pravidla pořadí

  • Mikroúlohy: vykonají se po každém callbacku a před přechodem do další fáze. Patří sem reakce Promise a process.nextTick. nextTick má navíc přednost i před mikroúlohami Promise – jeho nadměrné používání může vyhladovět event loop.
  • Makroúlohy: položky ve frontách jednotlivých fází (timers, I/O, check…). setTimeout a setImmediate mohou mít různé pořadí podle toho, v jaké fázi byly naplánovány.

Časování: setTimeout vs. setImmediate vs. nextTick

  • setTimeout(fn, 0): minimální zpoždění je orientační (clamping), callback se spustí ve fázi timers po uplynutí časového limitu a až po zpracování mikroúloh.
  • setImmediate(fn): callback se spustí ve fázi check hned po fázi poll. Pokud se naplánuje z I/O callbacku, obvykle předběhne setTimeout(..., 0).
  • process.nextTick(fn): mikroúloha s nejvyšší prioritou; používejte ji střídmě (např. kvůli kompatibilitě API), jinak hrozí vyhladovění event loopu.

Asynchronní I/O a fond vláken libuv

Sockety a většina síťového I/O jsou skutečně neblokující a řídí je systémový poller (epoll/kqueue/IOCP). Operace se souborovým systémem (část modulu fs) se provádějí ve fondu vláken knihovny libuv (výchozí velikost bývá 4, nastavit ji lze přes UV_THREADPOOL_SIZE). Intenzivní práce s diskem proto může obsadit dostupná vlákna a zvýšit latenci – sledujte fronty a případně zvětšete fond vláken nebo použijte streaming a dávkové zpracování.

Streams a zpětný tlak (backpressure)

stream.Readable/Writable poskytují proudové API s řízením rychlosti. Důležité je respektovat návratovou hodnotu metody write() a počkat na událost 'drain'. Tím zabráníte přeplnění bufferů a stabilizujete latenci. Metoda pipe (readable.pipe(writable)) řeší zpětný tlak automaticky.

Programovací model: callbacky, Promise a async/await

  • Callbacky (historicky kontrakt error-first (err, data)) – stále se používají v některých API (např. fs).
  • Promise a async/await: moderní API (fs/promises, timers/promises) s přehledným řízením chyb pomocí try/catch. Pozor na paralelizaci: await v cyklu operace serializuje; použijte Promise.all/allSettled.

Garbage collector a výkon

V8 používá generační GC. Dlouhé pauzy snižují propustnost. Doporučená opatření: vyhýbejte se masivním alokacím v horkých smyčkách, recyklujte objekty tam, kde to dává smysl, při I/O upřednostňujte sdružování bufferů Buffer, měřte využití haldy pomocí nástrojů inspector a heap snapshots. U služeb citlivých na latenci je vhodné sledovat zpoždění event loopu a statistiky GC.

Moduly: CommonJS vs. ECMAScript Modules

Node podporuje CJS (require, module.exports) i ESM (import/export). Režim určují soubor package.json ("type": "module") a přípony souborů (.mjs/.cjs). Kombinování formátů vyžaduje zohlednit specifická pravidla (např. createRequire), proto v projektu upřednostňujte jednotný styl a explicitní mapy exportů (exports).

Worker Threads a Cluster

  • Worker Threads: skutečně paralelně zpracovávají úlohy náročné na CPU v rámci jednoho procesu, s izolovanými haldami, sdílenou pamětí (SharedArrayBuffer) a komunikací předáváním zpráv (message-passing). Používejte je pro kompresi, šifrování a transformace.
  • Cluster: více procesů sdílejících jeden port (round-robin). Zvyšuje propustnost na vícejádrových serverech, ale každý proces má vlastní haldu a event loop.

Nativní doplňky (Addons) a N-API

Pro kritické úseky kódu lze psát nativní moduly v C/C++ pomocí N-API, které stabilizuje ABI napříč verzemi Node.js. Hodí se pro obálky knihoven operačního systému, kryptografii, parsování nebo náročné výpočty. Dbejte na přenositelnost, správu paměti a bezpečnost při práci s vlákny.

HTTP server a síťové vzory

Server http/http2 je založený na událostech: každé spojení představuje socket, který zpracovává požadavky a odpovědi jako streamy. Optimalizace: keep-alive, opakované využití spojení, kompaktní hlavičky, podpora HTTP/2, komprese (zlib/brotli), správné nastavení cache-control a ETag. Zvažte ochranu pomocí WAF, omezení počtu požadavků, časové limity (headersTimeout, requestTimeout), omezení velikosti body a ochranu proti útokům typu slow-loris.

Bezpečnost: běhové prostředí i závislosti

  • Kontrolujte závislosti pomocí bezpečnostního auditu, zamykejte verze (lockfile) a minimalizujte útočnou plochu.
  • Chraňte se proti zneužití prototypů (Prototype Pollution), SSRF, ReDOS (vyhýbejte se patologickým regulárním výrazům), XSS v šablonách a path traversal.
  • V OpenSSL používejte bezpečné křivky a šifry, při odchozích spojeních ověřujte certifikáty a názvy hostitelů.

Observabilita: metriky, logy a tracing

Měřte zpoždění event loopu, počet otevřených handlerů, využití haldy a CPU, latenci endpointů a chybovost. Pro tracing použijte OpenTelemetry a W3C Trace Context. Zaznamenávejte strukturované logy (JSON) s korelačními ID a dbejte na ochranu osobních údajů.

Diagnostika a profilování

  • Inspector (--inspect, DevTools) pro krokování a profilování CPU a haldy.
  • perf/0x/clinic pro plamenné grafy a hledání výkonnostních úzkých míst.
  • Async Hooks pro sledování životního cyklu asynchronních zdrojů (používejte opatrně – mohou snížit výkon).

Správa procesu: signály, fronty a vypínání

Implementujte řízené vypnutí: zachytávejte SIGTERM, přestaňte přijímat nová spojení, počkejte na dokončení rozpracovaných požadavků, uvolněte zdroje a ukončete proces s kódem 0. V kontejnerovém prostředí zohledněte sondy liveness/readiness a zpětný tlak z ingress vrstvy.

Zásady výkonu a anti-patterny

  • Neprovádějte smyčky náročné na CPU v hlavním vlákně – použijte Worker Threads nebo přesuňte zátěž na jiné služby (offload).
  • Vyhýbejte se synchronním API (fs.readFileSync) při zpracování požadavků.
  • Upřednostňujte streamy před načítáním celého obsahu do paměti.
  • Zpracovávejte I/O dávkově a slučujte je (coalesce), využívejte sdružování spojení.
  • Měřte, profilujte a iterujte – odhad není metrika.

Správa balíčků a nasazení

package.json definuje skripty (start, build), mapy exportů, typ modulu, engines a bin. Pro rychlejší start používejte prebuild, tree-shaking a minimalizujte kroky transpile za běhu. V produkci uzamkněte lockfile, nastavte NODE_ENV=production, hlídejte velikost image a nastavte limity paměti.

Kompatibilita a verze Node.js

Pro produkční prostředí zvolte řadu LTS. Sledujte zásadní změny podle semver (např. výchozí chování ESM, nová API v fetch, stabilizaci test runner). Testujte s více verzemi pomocí CI matice a v engines definujte minimální podporované verze.

Typické architektonické vzory

  • API Gateway/Backend-for-Frontend: agregace dat, cache, omezení počtu požadavků, autorizace.
  • Realtime: WebSocket/SSE, pub-sub, škálování přes Redis/Cloud pubsub.
  • Job workers: fronty (BullMQ, RabbitMQ), idempotence, deduplikace a plánování.

Závěr

Node.js kombinuje jednovláknový event loop s neblokujícím I/O a výkonnou standardní knihovnou. Pochopení fází smyčky, rozdílu mezi mikroúlohami a makroúlohami, role fondu vláken a práce se streamy je zásadní pro stabilní a rychlé služby. Správně navržená architektura – s observabilitou, bezpečností, worker threads pro úlohy náročné na CPU a pečlivým vypínáním – umožňuje škálovat od jednoduchých API až po globální realtime platformy.