Node.js Performance Fundamentals
Node.js is single-threaded but highly concurrent — understanding its event loop is key to optimization.
CPU Profiling with --prof
# Generate V8 profile
node --prof app.js
# Process the profile
node --prof-process isolate-*.log > profile.txt
# Or use clinic.js
npm install -g clinic
clinic doctor -- node app.js
clinic flame -- node app.js
Event Loop Monitoring
import { monitorEventLoopDelay } from 'perf_hooks';
const h = monitorEventLoopDelay({ resolution: 20 });
h.enable();
setInterval(() => {
console.log({
min: h.min / 1e6 + 'ms',
max: h.max / 1e6 + 'ms',
mean: h.mean / 1e6 + 'ms',
p99: h.percentile(99) / 1e6 + 'ms',
});
h.reset();
}, 5000);
Cluster Mode
// cluster.js
import cluster from 'cluster';
import os from 'os';
import { createServer } from './app.js';
if (cluster.isPrimary) {
const numCPUs = os.cpus().length;
console.log(`Primary ${process.pid}: forking ${numCPUs} workers`);
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died — restarting`);
cluster.fork(); // Auto-restart
});
} else {
const app = createServer();
app.listen(3000, () => {
console.log(`Worker ${process.pid}: listening on port 3000`);
});
}
Worker Threads for CPU-Bound Tasks
// main.js
import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';
import { cpus } from 'os';
function runWorker(data) {
return new Promise((resolve, reject) => {
const worker = new Worker('./worker.js', { workerData: data });
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) reject(new Error(`Worker exited with code ${code}`));
});
});
}
// Parallel processing
const results = await Promise.all(
chunks.map(chunk => runWorker({ data: chunk }))
);
// worker.js
import { parentPort, workerData } from 'worker_threads';
function heavyComputation(data) {
// CPU-intensive work: image processing, crypto, parsing
return data.reduce((acc, n) => acc + n * n, 0);
}
parentPort.postMessage(heavyComputation(workerData.data));
Worker Thread Pool
import { Worker } from 'worker_threads';
import { EventEmitter } from 'events';
class WorkerPool extends EventEmitter {
constructor(workerPath, poolSize = cpus().length) {
super();
this.workers = [];
this.queue = [];
for (let i = 0; i < poolSize; i++) {
this.addWorker(workerPath);
}
}
addWorker(workerPath) {
const worker = new Worker(workerPath);
worker.on('message', (result) => {
worker.busy = false;
const { resolve } = worker.currentTask;
resolve(result);
this.processQueue(worker);
});
worker.busy = false;
this.workers.push(worker);
}
run(data) {
return new Promise((resolve, reject) => {
const freeWorker = this.workers.find(w => !w.busy);
const task = { data, resolve, reject };
if (freeWorker) {
this.runTask(freeWorker, task);
} else {
this.queue.push(task);
}
});
}
runTask(worker, task) {
worker.busy = true;
worker.currentTask = task;
worker.postMessage(task.data);
}
processQueue(worker) {
if (this.queue.length > 0) {
this.runTask(worker, this.queue.shift());
}
}
}
Memory Leak Detection
# Use node --inspect + Chrome DevTools
node --inspect app.js
# Or use heapdump
npm install heapdump
# Take heap snapshot on demand
process.kill(process.pid, 'SIGUSR2');
// Detect memory leaks with memwatch-next
import memwatch from '@memwatch/node';
memwatch.on('leak', (info) => {
console.error('Memory leak detected:', info);
});
memwatch.on('stats', (stats) => {
console.log('GC stats:', stats);
});
Stream Processing for Large Data
import { Transform, pipeline } from 'stream';
import { promisify } from 'util';
const pipelineAsync = promisify(pipeline);
// Process large CSV without loading into memory
await pipelineAsync(
fs.createReadStream('large-file.csv'),
csv.parse({ headers: true }),
new Transform({
objectMode: true,
transform(row, encoding, callback) {
// Process each row
const processed = transformRow(row);
callback(null, processed);
}
}),
new Transform({
objectMode: true,
transform(row, encoding, callback) {
this.push(JSON.stringify(row) + '
');
callback();
}
}),
fs.createWriteStream('output.jsonl')
);
HTTP/2 and Keep-Alive
import http2 from 'http2';
import fs from 'fs';
const server = http2.createSecureServer({
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem'),
});
// Express with http2
import spdy from 'spdy';
const server = spdy.createServer(
{ key, cert },
app
);
Performance Checklist
- Use
--max-old-space-sizeto increase heap when needed - Avoid synchronous I/O (
fs.readFileSyncetc.) in hot paths - Use
Buffer.allocUnsafe()for performance-critical buffer ops - Prefer
Mapover plain objects for frequent add/delete - Use
Promise.allfor parallel async operations - Enable
keep-alivein HTTP agents - Use connection pooling for databases
- Cache expensive computations with LRU cache