# Performa Node.js: Event Loop, Clustering, dan Optimasi di Tahun 2026 > Panduan lengkap tentang performa Node.js mencakup mekanisme event loop, clustering multi-proses, worker threads, optimasi memori, dan monitoring produksi dengan OpenTelemetry. Dilengkapi contoh kode siap pakai untuk wawancara teknis dan penerapan di lingkungan produksi. - Published: 2026-04-25 - Updated: 2026-04-25 - Author: SharpSkill - Tags: node.js, performance, event-loop, clustering, optimization, worker-threads - Reading time: 9 min --- Node.js telah menjadi fondasi utama bagi ribuan layanan backend berskala besar di seluruh dunia. Arsitektur event-driven dan model single-threaded yang dimilikinya memungkinkan penanganan puluhan ribu koneksi secara bersamaan tanpa overhead pembuatan thread untuk setiap permintaan. Namun, kemampuan tersebut baru tercapai secara optimal ketika pengembang memahami secara mendalam bagaimana event loop bekerja, kapan harus menerapkan clustering, dan teknik optimasi apa yang relevan untuk lingkungan produksi. Di tahun 2026, dengan hadirnya Node.js 24 LTS dan peningkatan signifikan pada V8 engine, lanskap performa Node.js mengalami perubahan penting. Fitur-fitur baru seperti peningkatan performa garbage collector, stabilisasi Permission Model, dan penyempurnaan diagnostics channel membuka peluang optimasi yang sebelumnya tidak tersedia. Artikel ini membahas secara komprehensif setiap aspek performa Node.js -- mulai dari mekanisme internal event loop hingga strategi monitoring di lingkungan produksi -- disertai contoh kode yang dapat langsung diterapkan. Pemahaman mendalam tentang topik-topik ini bukan sekadar kebutuhan teknis harian. Dalam sesi wawancara teknis untuk posisi backend engineer, pertanyaan seputar event loop, clustering, dan optimasi performa Node.js merupakan materi yang sangat sering diujikan. Penguasaan konsep-konsep ini memberikan keunggulan kompetitif yang nyata. > **Poin Penting** > > Performa Node.js ditentukan oleh tiga pilar utama: pemahaman fase-fase event loop untuk mencegah blocking, penerapan clustering dan worker threads untuk memanfaatkan seluruh inti CPU, serta monitoring berkelanjutan menggunakan instrumentasi seperti OpenTelemetry. Menguasai ketiganya adalah kunci membangun layanan backend yang tangguh dan responsif. ## Memahami Event Loop Node.js Secara Mendalam Event loop adalah jantung dari runtime Node.js. Berbeda dengan server tradisional yang membuat thread baru untuk setiap koneksi masuk, Node.js menggunakan satu thread utama yang secara terus-menerus menjalankan siklus fase-fase tertentu. Setiap fase memiliki antrian callback yang menunggu untuk dieksekusi. Urutan fase-fase dalam event loop adalah sebagai berikut: 1. **Timers** -- mengeksekusi callback dari `setTimeout` dan `setInterval` yang telah melewati batas waktu 2. **Pending Callbacks** -- mengeksekusi callback I/O yang ditunda dari iterasi sebelumnya 3. **Idle/Prepare** -- fase internal yang digunakan oleh Node.js 4. **Poll** -- mengambil event I/O baru dan mengeksekusi callback terkait I/O 5. **Check** -- mengeksekusi callback dari `setImmediate` 6. **Close Callbacks** -- mengeksekusi callback penutupan seperti `socket.on('close')` Di antara setiap transisi fase, Node.js memproses **microtask queue** yang terdiri dari callback `process.nextTick` dan Promise yang telah resolved. Perilaku ini krusial untuk dipahami karena microtask selalu diproses sebelum event loop melanjutkan ke fase berikutnya. Berikut adalah demonstrasi urutan eksekusi fase-fase tersebut: ```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')); ``` Dari kode di atas, `process.nextTick` selalu dieksekusi sebelum Promise karena nextTick queue diproses terlebih dahulu dalam setiap iterasi microtask. Memahami urutan ini sangat penting saat mendesain sistem yang memerlukan kontrol presisi terhadap waktu eksekusi callback. ## Monitoring Event Loop di Lingkungan Produksi Mengetahui bahwa event loop terblokir adalah langkah pertama dalam mendiagnosis masalah performa. Node.js menyediakan modul `perf_hooks` yang memungkinkan pemantauan latensi event loop dengan resolusi tinggi. Metrik ini menjadi indikator utama kesehatan sebuah aplikasi Node.js. Event loop utilization (ELU) mengukur rasio waktu yang dihabiskan event loop dalam kondisi aktif dibandingkan idle. Nilai ELU mendekati 1.0 mengindikasikan bahwa event loop hampir tidak pernah menganggur -- situasi yang mengkhawatirkan karena permintaan baru harus menunggu lebih lama untuk diproses. ```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); ``` Dalam praktik produksi, ambang batas ELU sebesar 0.7 (70%) dan latensi p99 di atas 100ms merupakan sinyal peringatan yang memerlukan investigasi segera. Metrik-metrik ini sebaiknya dikirimkan ke sistem monitoring terpusat untuk memungkinkan pembuatan alert dan analisis tren historis. ## Pola Blocking dan Solusinya Event loop yang terblokir adalah musuh utama performa Node.js. Operasi sinkron yang memakan waktu lama pada thread utama akan menghentikan pemrosesan seluruh permintaan lain yang sedang menunggu. Dua penyebab paling umum adalah parsing JSON berukuran besar dan operasi kriptografi sinkron. Berikut adalah pola-pola yang harus dihindari beserta solusi yang tepat: ```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')); }); }); } ``` Prinsip dasarnya sederhana: setiap operasi yang memakan waktu lebih dari 1-2 milidetik pada thread utama harus dipindahkan ke varian asinkron atau didelegasikan ke worker thread. Parsing JSON berukuran besar sebaiknya dilakukan secara streaming, sementara operasi kriptografi harus selalu menggunakan API asinkron yang disediakan oleh modul `crypto`. ## Clustering: Memanfaatkan Seluruh Inti CPU Node.js secara bawaan berjalan pada satu inti CPU. Pada server modern yang memiliki 8, 16, atau bahkan 64 inti, menjalankan satu proses Node.js berarti membuang sebagian besar kapasitas komputasi yang tersedia. Modul `cluster` mengatasi keterbatasan ini dengan memungkinkan pembuatan beberapa proses worker yang berbagi port yang sama. Arsitektur clustering terdiri dari satu proses primary yang bertugas mengelola worker dan mendistribusikan koneksi masuk. Setiap worker adalah proses Node.js terpisah dengan event loop-nya sendiri, sehingga crash pada satu worker tidak memengaruhi worker lainnya. ```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)); }); } ``` Graceful shutdown merupakan aspek yang sering diabaikan namun sangat krusial. Ketika proses primary menerima sinyal `SIGTERM`, setiap worker harus menyelesaikan permintaan yang sedang diproses sebelum berhenti. Pendekatan ini mencegah terputusnya koneksi klien selama proses deployment atau scaling. Dalam praktiknya, jumlah worker yang optimal biasanya sama dengan jumlah inti CPU yang tersedia. Namun, untuk aplikasi yang bersifat I/O-intensive, penggunaan worker melebihi jumlah inti CPU dapat memberikan manfaat karena setiap worker sering berada dalam kondisi idle menunggu respons I/O. ## Worker Threads: Komputasi Paralel Tanpa Proses Terpisah Berbeda dengan clustering yang membuat proses terpisah, worker threads memungkinkan eksekusi kode JavaScript secara paralel dalam proses yang sama. Worker threads ideal untuk tugas-tugas CPU-intensive seperti pemrosesan gambar, kompresi data, atau kalkulasi kompleks yang akan memblokir event loop jika dijalankan pada thread utama. Implementasi worker pool yang dapat digunakan kembali memberikan abstraksi yang bersih untuk mendelegasikan tugas berat: ```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 }; ``` Berikut adalah contoh worker thread yang melakukan pemrosesan gambar menggunakan library `sharp`: ```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 }); }); ``` Pola ini memisahkan tugas berat dari thread utama secara efektif. Thread utama tetap responsif terhadap permintaan HTTP masuk, sementara pemrosesan gambar berjalan secara paralel pada worker thread terpisah. Mekanisme antrian pada `WorkerPool` memastikan bahwa jumlah tugas yang berjalan secara bersamaan tidak melebihi jumlah worker yang tersedia. > **Jangan Campur Cluster dengan Worker Threads Secara Sembarangan** > > Menjalankan worker threads di dalam proses yang sudah di-cluster dapat menyebabkan jumlah thread meledak secara eksponensial. Jika sebuah server memiliki 8 inti CPU dengan 8 proses cluster, dan setiap proses membuat pool berisi 8 worker thread, total thread yang aktif menjadi 64 -- jauh melebihi kapasitas optimal. Gunakan clustering untuk mendistribusikan koneksi HTTP dan worker threads untuk tugas CPU-intensive spesifik, dengan jumlah thread per worker yang dibatasi (biasanya 2-4 per proses cluster). ## Optimasi Memori dan Tekanan Garbage Collector Garbage collector (GC) V8 bekerja dalam dua generasi: young generation untuk objek berumur pendek dan old generation untuk objek yang bertahan melewati beberapa siklus GC. Tekanan GC yang tinggi -- ditandai dengan alokasi objek baru secara masif dalam loop yang padat -- menyebabkan jeda yang mengganggu responsivitas aplikasi. Strategi utama untuk mengurangi tekanan GC meliputi penggunaan kembali buffer, minimalisasi alokasi objek dalam hot path, dan konfigurasi parameter V8 yang sesuai dengan profil beban kerja: ```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) ``` Flag `--max-old-space-size` menentukan batas memori old generation. Untuk aplikasi yang menangani dataset besar, meningkatkan nilai ini dari default 1.7GB ke 4GB atau lebih dapat mencegah crash akibat out-of-memory. Sementara itu, `--max-semi-space-size` mengontrol ukuran young generation -- memperbesar nilainya mengurangi frekuensi minor GC pada aplikasi dengan alokasi objek yang sangat tinggi. Pemantauan heap secara berkala menggunakan `process.memoryUsage()` memungkinkan deteksi dini memory leak. Tren peningkatan heap yang konsisten tanpa penurunan merupakan indikator kuat adanya kebocoran memori yang memerlukan investigasi lebih lanjut. ## Monitoring Produksi dengan OpenTelemetry OpenTelemetry telah menjadi standar de facto untuk observability di ekosistem Node.js. Dengan instrumentasi otomatis, setiap permintaan HTTP, query database, dan panggilan DNS dapat dilacak tanpa perubahan kode aplikasi. Metrik event loop lag yang digabungkan dengan tracing memberikan visibilitas menyeluruh terhadap performa sistem. ```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); ``` Konfigurasi di atas mengekspos metrik dalam format Prometheus pada port 9464, yang dapat di-scrape oleh Prometheus dan divisualisasikan melalui Grafana. Instrumentasi otomatis menangkap metrik HTTP (latensi, throughput, error rate), sementara metrik kustom event loop lag memberikan insight tambahan yang spesifik untuk Node.js. Dalam arsitektur mikroservis, distributed tracing yang disediakan OpenTelemetry memungkinkan pelacakan sebuah permintaan dari awal hingga akhir melewati berbagai layanan. Kemampuan ini sangat berharga untuk mengidentifikasi bottleneck yang terjadi pada interaksi antar-layanan. ## Matriks Keputusan: Memilih Strategi yang Tepat Memilih antara clustering, worker threads, atau pendekatan lainnya bergantung pada karakteristik beban kerja. Tabel berikut menyajikan panduan keputusan berdasarkan skenario yang umum ditemui: | Skenario | Strategi | Alasan | |---|---|---| | API server dengan beban tinggi | Clustering (N proses) | Mendistribusikan koneksi HTTP ke seluruh inti CPU | | Pemrosesan gambar/video | Worker threads | Mengisolasi tugas CPU-intensive dari thread utama | | Parsing JSON berukuran besar | Streaming parser | Mencegah blocking event loop tanpa overhead thread | | Operasi kriptografi | API async bawaan | Sudah menggunakan thread pool libuv secara internal | | Kompresi data real-time | Worker threads | Memparalelkan kompresi tanpa memblokir respons HTTP | | Microservice I/O-intensive | Clustering + async I/O | Memaksimalkan throughput I/O di seluruh inti CPU | | Batch processing berkala | Worker threads + antrian | Memproses batch secara paralel tanpa mengganggu server utama | Secara umum, clustering adalah pilihan pertama untuk meningkatkan throughput HTTP, sementara worker threads lebih tepat untuk tugas komputasi spesifik yang berdurasi panjang. Keduanya dapat dikombinasikan dengan batasan yang jelas pada jumlah total thread. > **Peningkatan di Node.js 24 LTS** > > Node.js 24 LTS yang dirilis tahun 2026 menghadirkan beberapa peningkatan performa yang signifikan. V8 engine versi terbaru memberikan peningkatan throughput garbage collector hingga 15% untuk beban kerja umum. Diagnostics channel yang telah distabilkan memungkinkan monitoring non-invasif tanpa overhead tambahan. Permission Model yang telah keluar dari status eksperimental memberikan lapisan keamanan tambahan tanpa dampak performa yang berarti. Selain itu, perbaikan pada HTTP parser dan peningkatan efisiensi `fetch` API menjadikan Node.js 24 pilihan yang sangat kuat untuk layanan produksi baru. ## Checklist Praktis Optimasi Performa Node.js Berikut adalah daftar periksa yang dapat digunakan sebagai panduan saat mengoptimalkan aplikasi Node.js untuk lingkungan produksi: **Event Loop** - Identifikasi dan eliminasi semua operasi sinkron pada jalur permintaan (request path) - Implementasikan monitoring ELU dengan ambang batas alert pada 70% - Gunakan streaming parser untuk payload JSON di atas 1MB - Selalu gunakan varian asinkron untuk operasi kriptografi **Clustering dan Worker Threads** - Aktifkan clustering dengan jumlah worker sesuai inti CPU yang tersedia - Implementasikan graceful shutdown pada proses primary dan setiap worker - Gunakan worker threads untuk tugas CPU-intensive dengan durasi di atas 10ms - Batasi total thread (cluster workers x thread pool) agar tidak melebihi 2x jumlah inti CPU **Memori dan GC** - Konfigurasikan `--max-old-space-size` sesuai ketersediaan RAM server - Minimalkan alokasi objek dalam hot loop -- gunakan kembali buffer dan array - Pantau tren heap secara berkala untuk deteksi dini memory leak - Pertimbangkan `--max-semi-space-size` untuk aplikasi dengan alokasi tinggi **Monitoring dan Observability** - Integrasikan OpenTelemetry dengan instrumentasi otomatis - Ekspos metrik event loop lag, ELU, heap usage, dan HTTP latency - Konfigurasikan alert untuk p99 latency, error rate, dan ELU - Terapkan distributed tracing untuk arsitektur mikroservis ## Kesimpulan Performa Node.js bukan sekadar tentang menulis kode yang cepat -- melainkan tentang memahami mekanisme internal runtime dan menerapkan strategi yang tepat untuk setiap jenis beban kerja. Event loop yang dipahami dengan baik memungkinkan pencegahan blocking sebelum terjadi. Clustering memanfaatkan kapasitas penuh hardware server. Worker threads mengisolasi komputasi berat tanpa mengorbankan responsivitas. Dan monitoring yang komprehensif memberikan visibilitas untuk mengidentifikasi serta menyelesaikan masalah performa sebelum berdampak pada pengguna. Untuk memperdalam pemahaman tentang topik-topik ini, tersedia beberapa sumber belajar yang relevan. Track teknologi [Node.js & NestJS](/technologies/node-nestjs) menyediakan jalur pembelajaran terstruktur yang mencakup seluruh aspek pengembangan backend dengan Node.js. Modul [middleware and interceptors](/technologies/node-nestjs/interview-questions/middleware-interceptors) membahas pola-pola arsitektural yang sering muncul dalam wawancara teknis. Selain itu, panduan [pertanyaan wawancara backend Node.js](/blog/node-nestjs/nodejs-backend-interview-questions) memberikan gambaran komprehensif tentang materi yang diuji dalam sesi wawancara untuk posisi backend engineer. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/node-nestjs/nodejs-performance-event-loop-clustering-optimization