Node.js Performance Optimization Guide

Node.js powers high-throughput web applications, but performance requires understanding its single-threaded event loop and non-blocking I/O model.


Event Loop Fundamentals


The event loop processes callbacks in phases: timers, I/O callbacks, idle/prepare, poll, check (setImmediate), and close callbacks. Each phase has a FIFO queue of callbacks. Blocking any phase delays all subsequent callbacks.


Avoid blocking the event loop. CPU-intensive operations (JSON parsing, cryptography, template rendering) block all other requests. Offload CPU work to Worker Threads, child processes, or dedicated microservices.


Profiling


Use the built-in --prof flag for V8 CPU profiling. Generate flame graphs with --prof-process. Node.js --inspect enables Chrome DevTools profiling with heap snapshots and CPU profiles.


Clinic.js provides visualization for event loop lag, garbage collection, and heap growth. Use autocannon or wrk for load testing. Profile in production-like environments—performance characteristics differ between development and production.


Memory Management


Monitor memory with process.memoryUsage(). Watch for heap growth between garbage collection cycles. Use heap snapshots (node --inspect, then Memory tab) to identify memory leaks.


Common leak sources: global variables, event listeners not removed, closures retaining references, and cache without size limits. Use the --max-old-space-size flag to limit heap size and detect leaks earlier via OOM crashes.


Async Performance


Use native Promises instead of callback patterns. Native Promises are optimized in V8 and outperform bluebird in modern Node.js versions. Use async/await for readability without performance cost.


Avoid mixing promise styles. Use util.promisify for callback-based APIs. Limit concurrent async operations with p-limit or similar. Unhandled promise rejections crash Node.js in recent versions—handle all promise rejections.


Production Tuning


Set NODE_ENV=production for framework optimizations. Configure max-old-space-size to 75% of available memory. Use clustering (cluster module) or PM2 for multi-core utilization. Implement graceful shutdown with SIGTERM handling.


Monitor event loop lag (>40ms indicates overload). Use the proses monitoring module for Node.js-specific metrics. Set up GC metrics monitoring—frequent GC cycles indicate memory pressure.