# Wydajność Node.js: Event Loop, Klasteryzacja i Optymalizacja w 2026 > Optymalizacja wydajności Node.js poprzez zarządzanie pętlą zdarzeń, strategie klasteryzacji i wątki robocze. Praktyczne wzorce dla wysoko obciążonych aplikacji Node.js w 2026 roku. - Published: 2026-04-25 - Updated: 2026-04-25 - Author: SharpSkill - Tags: node.js, performance, event-loop, clustering, optimization, worker-threads - Reading time: 9 min --- Optymalizacja wydajności Node.js zaczyna się od zrozumienia pętli zdarzeń (event loop) — jednowątkowego mechanizmu obsługującego wszystkie asynchroniczne operacje I/O. W systemach produkcyjnych działających na Node.js 22 LTS lub Node.js 24, złe zarządzanie pętlą zdarzeń pozostaje główną przyczyną skoków opóźnień, utraconych połączeń i kaskadowych awarii pod obciążeniem. Ten przewodnik obejmuje trzy filary wydajności Node.js: wewnętrzne działanie pętli zdarzeń, skalowanie wielordzeniowe z klastrami i wątkami roboczymi oraz praktyczne wzorce optymalizacji stosowane w systemach o wysokiej przepustowości. > **Kluczowy wniosek** > > Pętla zdarzeń przetwarza wywołania zwrotne w ściśle określonej kolejności faz: timery, oczekujące wywołania zwrotne, idle/prepare, poll, check i close callbacks. Zablokowanie jakiejkolwiek pojedynczej fazy wstrzymuje całą aplikację. Monitorowanie wykorzystania pętli zdarzeń za pomocą `performance.eventLoopUtilization()` to najskuteczniejsze narzędzie diagnostyczne dla problemów wydajnościowych Node.js. ## Jak pętla zdarzeń Node.js przetwarza żądania Pętla zdarzeń nie jest prostą kolejką. Działa w sześciu odrębnych fazach, z których każda odpowiada za określoną kategorię wywołań zwrotnych. Zrozumienie tej architektury fazowej wyjaśnia, dlaczego pewne wzorce powodują opóźnienia, a inne nie. ```javascript // event-loop-phases.js // Demonstrating phase execution order const fs = require('fs'); // Phase 1: Timers — executes setTimeout/setInterval callbacks setTimeout(() => console.log('1. Timer phase'), 0); // Phase 4: Poll — executes I/O callbacks fs.readFile(__filename, () => { console.log('2. Poll phase (I/O callback)'); // Phase 5: Check — executes setImmediate callbacks setImmediate(() => console.log('3. Check phase (setImmediate)')); // Phase 1 again: Timer scheduled from within I/O setTimeout(() => console.log('4. Timer phase (from I/O)'), 0); }); // Microtask — runs between every phase transition Promise.resolve().then(() => console.log('Microtask: Promise')); process.nextTick(() => console.log('Microtask: nextTick')); ``` Kolejność wyjścia ujawnia priorytet faz: `nextTick` uruchamia się przed obietnicami, obietnice przed timerami, a `setImmediate` zawsze odpala się po wywołaniach zwrotnych I/O w fazie check. Ta kolejność ma znaczenie podczas projektowania obsługi żądań wrażliwych na opóźnienia. ## Wykrywanie blokowania pętli zdarzeń w produkcji Zablokowana pętla zdarzeń objawia się rosnącym opóźnieniem p99 na długo przed degradacją średnich czasów odpowiedzi. Wbudowane API `performance.eventLoopUtilization()`, stabilne od Node.js 16, zapewnia najbardziej niezawodny mechanizm detekcji. ```javascript // event-loop-monitor.js // Production-grade event loop monitoring const { performance, monitorEventLoopDelay } = require('perf_hooks'); // High-resolution event loop delay histogram const histogram = monitorEventLoopDelay({ resolution: 20 }); histogram.enable(); // Track utilization over intervals let previous = performance.eventLoopUtilization(); setInterval(() => { const current = performance.eventLoopUtilization(previous); previous = performance.eventLoopUtilization(); const metrics = { // Ratio of time the loop spent active vs idle (0-1) utilization: current.utilization.toFixed(3), // Delay percentiles in milliseconds p50: (histogram.percentile(50) / 1e6).toFixed(2), p99: (histogram.percentile(99) / 1e6).toFixed(2), max: (histogram.max / 1e6).toFixed(2), }; // Alert when utilization exceeds 70% or p99 > 100ms if (current.utilization > 0.7 || histogram.percentile(99) > 100e6) { console.warn('EVENT_LOOP_SATURATED', metrics); } histogram.reset(); }, 5000); ``` Wykorzystanie powyżej 0.7 (70%) sygnalizuje, że pętla zdarzeń spędza więcej czasu na wykonywaniu wywołań zwrotnych niż na oczekiwaniu na I/O. Przy tym progu przychodzące połączenia zaczynają się kolejkować, a opóźnienia końcowe rosną wykładniczo. ## Typowe blokery pętli zdarzeń i sposoby ich naprawy Trzy wzorce odpowiadają za większość incydentów blokowania pętli zdarzeń w produkcyjnych aplikacjach Node.js: synchroniczne operacje JSON na dużych ładunkach, obliczenia intensywne procesorowo w obsłudze żądań oraz nieograniczone wyrażenia regularne. ```javascript // blocking-patterns.js // Anti-patterns and their solutions // PROBLEM: JSON.parse blocks on large payloads const largePayload = Buffer.alloc(50 * 1024 * 1024); // 50MB // JSON.parse(largePayload.toString()); // Blocks event loop 200-500ms // SOLUTION: Stream-parse large JSON with a streaming parser const { Transform } = require('stream'); const JSONStream = require('jsonstream2'); function processLargeJSON(readableStream) { return new Promise((resolve, reject) => { const results = []; readableStream .pipe(JSONStream.parse('items.*')) // Stream-parse array items .on('data', (item) => results.push(item)) .on('end', () => resolve(results)) .on('error', reject); }); } // PROBLEM: Synchronous crypto in request path // const hash = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512'); // SOLUTION: Use async variant const crypto = require('crypto'); async function hashPassword(password, salt) { return new Promise((resolve, reject) => { crypto.pbkdf2(password, salt, 100000, 64, 'sha512', (err, key) => { if (err) reject(err); else resolve(key.toString('hex')); }); }); } ``` Asynchroniczny wariant `pbkdf2` przenosi pracę CPU do puli wątków libuv, utrzymując pętlę zdarzeń wolną do przetwarzania innych żądań. Ta pojedyncza zmiana zredukowała opóźnienie p99 z 900ms do 120ms w udokumentowanym incydencie produkcyjnym w sektorze fintech. ## Skalowanie Node.js na wielu rdzeniach CPU z modułem Cluster Pojedynczy proces Node.js wykorzystuje jeden rdzeń CPU. Na 16-rdzeniowym serwerze produkcyjnym oznacza to, że 93% dostępnej mocy obliczeniowej pozostaje bezczynne. Wbudowany moduł `cluster` uruchamia procesy robocze współdzielące jeden port, dystrybuując przychodzące połączenia na wszystkie rdzenie. ```javascript // cluster-setup.js // Production clustering with graceful shutdown const cluster = require('cluster'); const os = require('os'); const process = require('process'); const WORKER_COUNT = parseInt(process.env.WORKERS) || os.cpus().length; if (cluster.isPrimary) { console.log(`Primary ${process.pid} starting ${WORKER_COUNT} workers`); // Fork workers for each CPU core for (let i = 0; i < WORKER_COUNT; i++) { cluster.fork(); } // Restart crashed workers automatically cluster.on('exit', (worker, code, signal) => { if (!worker.exitedAfterDisconnect) { console.error(`Worker ${worker.process.pid} died (${signal || code}). Restarting...`); cluster.fork(); } }); // Graceful shutdown on SIGTERM process.on('SIGTERM', () => { console.log('Primary received SIGTERM. Shutting down workers...'); for (const id in cluster.workers) { cluster.workers[id].disconnect(); } }); } else { // Worker process — start the actual HTTP server const http = require('http'); const server = http.createServer((req, res) => { res.writeHead(200); res.end(`Handled by worker ${process.pid}\n`); }); server.listen(3000, () => { console.log(`Worker ${process.pid} listening on port 3000`); }); // Graceful shutdown for individual worker process.on('SIGTERM', () => { server.close(() => process.exit(0)); }); } ``` Każdy worker działa we własnym izolacie V8 z oddzielną pamięcią. Workery nie współdzielą stanu — dane sesji, pamięci podręczne i stan aplikacji muszą znajdować się w zewnętrznym magazynie, takim jak Redis lub PostgreSQL. Ta izolacja zapewnia również odporność na awarie: crash jednego workera nie wpływa na pozostałe. ## Wątki robocze do zadań intensywnych procesorowo Moduł `cluster` duplikuje cały proces. Wątki robocze (worker threads), wprowadzone w Node.js 10 i stabilne od Node.js 12, uruchamiają JavaScript w równoległych wątkach w ramach tego samego procesu. Współdzielą pamięć przez `SharedArrayBuffer` i przesyłają dane przez klonowanie strukturalne. Różnica ma znaczenie: klastry służą do skalowania serwerów HTTP związanych z I/O na wielu rdzeniach, a wątki robocze do odciążania operacji związanych z CPU od pętli zdarzeń. ```javascript // worker-pool.js // Reusable worker thread pool for CPU tasks const { Worker } = require('worker_threads'); const os = require('os'); class WorkerPool { constructor(workerScript, poolSize = os.cpus().length) { this.workers = []; this.queue = []; for (let i = 0; i < poolSize; i++) { this.workers.push({ busy: false, worker: new Worker(workerScript) }); } } execute(taskData) { return new Promise((resolve, reject) => { const available = this.workers.find(w => !w.busy); if (available) { this._runTask(available, taskData, resolve, reject); } else { // Queue task until a worker is free this.queue.push({ taskData, resolve, reject }); } }); } _runTask(entry, taskData, resolve, reject) { entry.busy = true; entry.worker.postMessage(taskData); const onMessage = (result) => { entry.busy = false; cleanup(); resolve(result); this._processQueue(); }; const onError = (err) => { entry.busy = false; cleanup(); reject(err); this._processQueue(); }; const cleanup = () => { entry.worker.removeListener('message', onMessage); entry.worker.removeListener('error', onError); }; entry.worker.on('message', onMessage); entry.worker.on('error', onError); } _processQueue() { if (this.queue.length === 0) return; const available = this.workers.find(w => !w.busy); if (available) { const { taskData, resolve, reject } = this.queue.shift(); this._runTask(available, taskData, resolve, reject); } } } module.exports = { WorkerPool }; ``` ```javascript // image-worker.js // Worker thread for CPU-intensive image processing const { parentPort } = require('worker_threads'); const sharp = require('sharp'); parentPort.on('message', async ({ inputPath, width, height }) => { const result = await sharp(inputPath) .resize(width, height) .webp({ quality: 80 }) .toBuffer(); parentPort.postMessage({ size: result.length, buffer: result }); }); ``` Pula workerów uruchamia wątki z wyprzedzeniem przy starcie i ponownie je wykorzystuje między żądaniami. Pozwala to uniknąć narzutu 30-50ms związanego z tworzeniem nowego wątku roboczego na żądanie. Dla przetwarzania obrazów, generowania PDF lub transformacji danych, ten wzorzec utrzymuje opóźnienie głównej pętli zdarzeń poniżej 5ms nawet przy ciągłym obciążeniu CPU. ## Optymalizacja pamięci i strojenie odśmiecacza V8 dzieli stertę na młodą generację (obiekty krótkotrwałe) i starą generację (obiekty długowieczne). Większość problemów wydajnościowych wynika z nadmiernych alokacji w młodej generacji, które wyzwalają częste przerwy mniejszego GC, lub z wycieków pamięci, które rozrastają starą generację do momentu, gdy przerwy głównego GC powodują widoczne skoki opóźnień. ```javascript // memory-optimization.js // Patterns that reduce GC pressure // ANTI-PATTERN: Creating objects in hot loops function processItemsBad(items) { return items.map(item => ({ id: item.id, name: item.name.trim(), score: calculateScore(item), // New object per iteration metadata: { processed: true, timestamp: Date.now() } })); } // OPTIMIZED: Reuse buffers and minimize allocations const reusableBuffer = Buffer.alloc(4096); function processItemsGood(items, output) { // Reuse the output array instead of creating new one output.length = 0; for (let i = 0; i < items.length; i++) { // Mutate in place when safe to do so output.push(items[i].id); } return output; } // Monitor heap usage for leak detection function checkMemory() { const used = process.memoryUsage(); return { heapUsedMB: Math.round(used.heapUsed / 1024 / 1024), heapTotalMB: Math.round(used.heapTotal / 1024 / 1024), externalMB: Math.round(used.external / 1024 / 1024), rsssMB: Math.round(used.rss / 1024 / 1024), }; } // V8 flags for production GC tuning // node --max-old-space-size=4096 --max-semi-space-size=128 app.js // --max-old-space-size: Set old generation limit (default ~1.7GB) // --max-semi-space-size: Increase young generation (default 16MB) ``` Zwiększenie `--max-semi-space-size` z domyślnych 16MB do 64-128MB zmniejsza częstotliwość mniejszych GC dla aplikacji z wysokim współczynnikiem alokacji. Jest to kompromis między pamięcią a niższą częstotliwością przerw GC — opłacalny na serwerach z 8GB+ RAM. ## Monitoring produkcyjny z OpenTelemetry w Node.js 24 OpenTelemetry stał się standardowym frameworkiem instrumentacji dla Node.js w 2026 roku. Runtime Node.js 24 zawiera ulepszone wsparcie profilowania z danymi inline cache w profilach CPU, co czyni analizę wydajności znacznie dokładniejszą. ```javascript // otel-setup.js // OpenTelemetry setup for Node.js performance monitoring const { NodeSDK } = require('@opentelemetry/sdk-node'); const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node'); const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus'); const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics'); const sdk = new NodeSDK({ metricReader: new PrometheusExporter({ port: 9464 }), instrumentations: [ getNodeAutoInstrumentations({ // Instrument HTTP, Express, DNS, fs, and more '@opentelemetry/instrumentation-fs': { enabled: false }, // Too noisy }), ], }); sdk.start(); // Custom event loop lag metric const { metrics } = require('@opentelemetry/api'); const meter = metrics.getMeter('app'); const eventLoopLag = meter.createHistogram('nodejs.event_loop.lag', { description: 'Event loop lag in milliseconds', unit: 'ms', }); // Report event loop lag every second const { monitorEventLoopDelay } = require('perf_hooks'); const h = monitorEventLoopDelay({ resolution: 10 }); h.enable(); setInterval(() => { eventLoopLag.record(h.percentile(99) / 1e6); h.reset(); }, 1000); ``` Ta konfiguracja eksportuje opóźnienie pętli zdarzeń, czas trwania żądań HTTP i czas rozwiązywania DNS jako metryki Prometheus. Ustawienie alertów na opóźnienie pętli zdarzeń p99 > 100ms wykrywa degradację, zanim użytkownicy ją zauważą. > **Pułapka Cluster + Worker Thread** > > Każdy worker klastra otrzymuje własną pulę wątków libuv (domyślnie 4 wątki). Na maszynie 16-rdzeniowej z 16 workerami klastra oznacza to 64 wątki libuv konkurujące o czas CPU. W środowiskach klastrowych należy ustawić `UV_THREADPOOL_SIZE` na 2-4 na workera i zarezerwować rdzenie dla wątków roboczych obsługujących zadania CPU. ## Wybór między klastrami, wątkami roboczymi i skalowaniem zewnętrznym Właściwa strategia skalowania zależy od profilu obciążenia. Macierz decyzyjna oparta na rzeczywistych wzorcach produkcyjnych: | Scenariusz | Strategia | Uzasadnienie | |----------|----------|--------| | HTTP API, głównie I/O | Moduł Cluster (1 worker na rdzeń) | Maksymalizuje przepustowość połączeń | | Przetwarzanie obrazów/wideo | Pula wątków roboczych (4-8 wątków) | Utrzymuje responsywność pętli zdarzeń | | Pipeline danych intensywny CPU | Worker threads + SharedArrayBuffer | Współdzielenie danych bez kopiowania | | Mikroserwisy na dużą skalę | Pody Kubernetes (kontenery jednoprocesowe) | Orkiestrator zarządza skalowaniem | | Mieszane I/O + CPU | Cluster + pula workerów na workera | Każdy worker odciąża CPU do wątków | Dla skonteneryzowanych wdrożeń na Kubernetes, uruchamianie pojedynczego procesu Node.js na kontener (bez klasteryzacji) jest często prostsze i bardziej przewidywalne. Orkiestrator obsługuje skalowanie horyzontalne, kontrole zdrowia i aktualizacje kroczące. Klasteryzacja ma wartość przy uruchamianiu na bare metal lub maszynach wirtualnych, gdzie pojedyncza maszyna musi maksymalizować wykorzystanie rdzeni. > **Zyski wydajnościowe Node.js 24** > > Node.js 24, wydany w 2025 roku, zawiera V8 12.4 z 8-12% lepszą przepustowością w obciążeniach API. Wbudowany `fetch()` używa teraz Undici 7.0 z domyślnym wsparciem HTTP/2 i HTTP/3. Model uprawnień pozwala również na ograniczenie dostępu do systemu plików i sieci per-worker dla obrony w głąb. ## Praktyczna lista kontrolna optymalizacji dla produkcji Stosowanie tych technik w kolejności wpływu — najpierw zdrowie pętli zdarzeń, potem skalowanie, następnie dostrajanie — przynosi mierzalne usprawnienia przy minimalnym ryzyku. - **Monitorowanie wykorzystania pętli zdarzeń** w sposób ciągły za pomocą `performance.eventLoopUtilization()` — alert przy progu 70% - **Profilowanie przed optymalizacją** — użycie `node --prof` lub śladów OpenTelemetry do identyfikacji rzeczywistych wąskich gardeł, nie domniemanych - **Przeniesienie operacji synchronicznych z głównego wątku** — `crypto.pbkdf2`, `JSON.parse` na dużych ładunkach, przetwarzanie obrazów — to wszystko powinno trafiać do wątków roboczych - **Ustawienie `UV_THREADPOOL_SIZE`** na podstawie obciążenia — domyślne 4 to za mało dla aplikacji z intensywnym DNS lub I/O plikowym, za dużo w konfiguracjach klastrowych - **Użycie strumieniowania dla dużych danych** — `JSONStream`, `csv-parser` i `stream.pipeline()` Node.js zapobiegają skokom pamięci na dużych ładunkach - **Rozgrzewanie pul workerów** przy starcie — unikanie opóźnienia cold-start przy pierwszych żądaniach do endpointów intensywnych procesorowo - **Strojenie flag sterty V8** dla wdrożenia — `--max-old-space-size` i `--max-semi-space-size` na podstawie dostępnej pamięci i wzorców alokacji - **Implementacja graceful shutdown** — drenowanie połączeń przy SIGTERM, zamykanie pul baz danych i pozwolenie na zakończenie żądań w locie przed zamknięciem procesu ## Podsumowanie Wydajność Node.js w 2026 roku koncentruje się na trzech fundamentach: - Pętla zdarzeń jest pojedynczym punktem awarii dla przepustowości — monitorowanie wykorzystania i eliminowanie blokerów ma największy wpływ na opóźnienia - Klasteryzacja skaluje obciążenia związane z I/O na wielu rdzeniach; wątki robocze odciążają zadania CPU od pętli zdarzeń — użycie obu razem zapewnia maksymalne wykorzystanie sprzętu - Monitoring produkcyjny z [OpenTelemetry](https://opentelemetry.io/docs/languages/js/) i narzędziami profilowania V8 zamienia domysły w dane — alerty należy ustawić na opóźnienie p99 pętli zdarzeń i tempo wzrostu sterty - Node.js 24 przynosi zyski wydajnościowe V8 12.4, stabilne HTTP/3 przez Undici 7 i dojrzały model uprawnień — aktualizacja z Node.js 20 lub wcześniejszych wersji zapewnia mierzalne usprawnienia przepustowości - Rozpoczęcie od `performance.eventLoopUtilization()` na dowolnej istniejącej aplikacji Node.js — wyniki często ujawniają pojedynczą optymalizację o największym wpływie Więcej materiałów do przygotowania na rozmowy kwalifikacyjne z Node.js i NestJS znajduje się w [ścieżce technologicznej Node.js & NestJS](/technologies/node-nestjs) oraz w [module middleware i interceptory](/technologies/node-nestjs/interview-questions/middleware-interceptors), który szczegółowo omawia wzorce architektury produkcyjnej. [Przewodnik po pytaniach na rozmowę kwalifikacyjną z Node.js backend](/blog/node-nestjs/nodejs-backend-interview-questions) obejmuje dodatkowe zaawansowane tematy często poruszane na rozmowach dla seniorów. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pl/blog/node-nestjs/nodejs-performance-event-loop-clustering-optimization