nodejs|April 02, 2026|4 min read

Performance Optimization and Profiling in Node.js

TL;DR

Profile with clinic.js and --inspect to find bottlenecks. Use cluster mode for CPU-bound work, worker threads for heavy computation, streams for large data. Cache aggressively, avoid synchronous operations, and monitor event loop lag in production.

Profiling First, Optimize Second

Never optimize blindly. Always profile to find the actual bottleneck.

Chrome DevTools Profiling

# Start with inspector
node --inspect server.js

# For production: connect only when needed
node --inspect=0.0.0.0:9229 server.js

Open chrome://inspect in Chrome, connect to your Node.js instance, and use the Performance and Memory tabs.

Clinic.js — Automated Diagnostics

npm install -g clinic

# Doctor: detect common issues (event loop delay, I/O, GC)
clinic doctor -- node server.js

# Flame: CPU flame graph (find what's burning CPU time)
clinic flame -- node server.js

# Bubbleprof: async flow visualization
clinic bubbleprof -- node server.js

Memory Leak Detection

Heap Snapshot Comparison

// Take heap snapshots at intervals
const v8 = require('v8');
const fs = require('fs');

function takeHeapSnapshot() {
  const snapshotStream = v8.writeHeapSnapshot();
  console.log(`Heap snapshot written to: ${snapshotStream}`);
}

// Expose via admin endpoint (protected!)
app.post('/admin/heap-snapshot', requireAdmin, (req, res) => {
  const filename = takeHeapSnapshot();
  res.json({ filename });
});

Common Memory Leak Patterns

// LEAK 1: Growing arrays/maps that are never cleaned
const cache = new Map();
app.get('/data/:id', async (req, res) => {
  const data = await fetchData(req.params.id);
  cache.set(req.params.id, data); // Grows forever!
  res.json(data);
});

// FIX: Use LRU cache with max size
const LRU = require('lru-cache');
const cache = new LRU({ max: 500, ttl: 1000 * 60 * 5 });

// LEAK 2: Event listeners not removed
class DataStream {
  subscribe(source) {
    source.on('data', this.handleData); // Never removed!
  }
  // FIX: Always remove listeners
  unsubscribe(source) {
    source.off('data', this.handleData);
  }
}

// LEAK 3: Closures holding references
function createHandler() {
  const hugeBuffer = Buffer.alloc(100 * 1024 * 1024); // 100MB

  return (req, res) => {
    // hugeBuffer is held in closure memory even if not used
    res.json({ status: 'ok' });
  };
}

Memory Monitoring

// Track memory usage over time
setInterval(() => {
  const usage = process.memoryUsage();
  logger.info({
    heapUsed: Math.round(usage.heapUsed / 1024 / 1024),
    heapTotal: Math.round(usage.heapTotal / 1024 / 1024),
    rss: Math.round(usage.rss / 1024 / 1024),
    external: Math.round(usage.external / 1024 / 1024),
  }, 'Memory usage (MB)');
}, 30000);

Cluster Mode

Use all CPU cores with the cluster module or PM2.

Cluster Mode Architecture

Native Cluster

const cluster = require('cluster');
const os = require('os');

if (cluster.isPrimary) {
  const numWorkers = os.cpus().length;
  console.log(`Master starting ${numWorkers} workers`);

  for (let i = 0; i < numWorkers; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code) => {
    console.log(`Worker ${worker.process.pid} died (code: ${code}), restarting...`);
    cluster.fork();
  });
} else {
  require('./server'); // Each worker runs the full server
}
// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'api',
    script: './server.js',
    instances: 'max',        // Use all CPUs
    exec_mode: 'cluster',
    max_memory_restart: '1G',
    env_production: {
      NODE_ENV: 'production',
      PORT: 3000,
    },
  }],
};
pm2 start ecosystem.config.js --env production
pm2 reload api   # Zero-downtime restart
pm2 monit        # Monitor in terminal

Worker Threads

For CPU-intensive tasks that would block the event loop.

// worker.js
const { parentPort, workerData } = require('worker_threads');

function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

const result = fibonacci(workerData.n);
parentPort.postMessage(result);

// main.js
const { Worker } = require('worker_threads');

function runInWorker(n) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./worker.js', { workerData: { n } });
    worker.on('message', resolve);
    worker.on('error', reject);
  });
}

app.get('/fibonacci/:n', async (req, res) => {
  const result = await runInWorker(parseInt(req.params.n));
  res.json({ result });
});

Worker Thread Pool

const { StaticPool } = require('node-worker-threads-pool');

const pool = new StaticPool({
  size: 4,
  task: './heavy-computation.js',
});

app.get('/process', async (req, res) => {
  const result = await pool.exec(req.body.data);
  res.json(result);
});

Streams for Memory Efficiency

// BAD: Loads entire file into memory
app.get('/export', async (req, res) => {
  const data = await db.query('SELECT * FROM orders'); // 1M rows = huge memory
  res.json(data.rows);
});

// GOOD: Stream the response
const { Transform } = require('stream');
const QueryStream = require('pg-query-stream');

app.get('/export', async (req, res) => {
  const client = await pool.connect();

  const query = new QueryStream('SELECT * FROM orders ORDER BY id');
  const dbStream = client.query(query);

  res.setHeader('Content-Type', 'application/json');
  res.write('[');

  let first = true;
  const transform = new Transform({
    objectMode: true,
    transform(row, encoding, callback) {
      const prefix = first ? '' : ',';
      first = false;
      callback(null, prefix + JSON.stringify(row));
    },
    flush(callback) {
      callback(null, ']');
    },
  });

  dbStream.pipe(transform).pipe(res);

  dbStream.on('end', () => client.release());
});

Common Performance Anti-Patterns

// 1. Synchronous operations in request handlers
app.get('/config', (req, res) => {
  const config = fs.readFileSync('config.json'); // BLOCKS event loop
  res.json(JSON.parse(config));
});
// FIX: Read async or cache at startup

// 2. Not using database indexes
// SELECT * FROM orders WHERE user_id = ? AND status = ?
// FIX: CREATE INDEX idx_orders_user_status ON orders(user_id, status);

// 3. N+1 queries
// FIX: Use JOINs, populate(), or DataLoader

// 4. Not compressing responses
const compression = require('compression');
app.use(compression()); // Gzip responses

// 5. Parsing large JSON on the main thread
// FIX: Use streaming JSON parsers for large payloads

// 6. Creating regex in hot paths
// BAD: new RegExp() on every request
// FIX: Compile regex once, reuse

Event Loop Monitoring

const { monitorEventLoopDelay } = require('perf_hooks');

const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();

// Expose metrics endpoint
app.get('/metrics', (req, res) => {
  res.json({
    eventLoop: {
      min: (histogram.min / 1e6).toFixed(2),
      max: (histogram.max / 1e6).toFixed(2),
      mean: (histogram.mean / 1e6).toFixed(2),
      p50: (histogram.percentile(50) / 1e6).toFixed(2),
      p99: (histogram.percentile(99) / 1e6).toFixed(2),
    },
    memory: process.memoryUsage(),
    uptime: process.uptime(),
  });
});

Performance Checklist

  1. Profile before optimizing — use clinic.js or Chrome DevTools
  2. Use cluster mode — leverage all CPU cores
  3. Stream large data — never buffer entire datasets in memory
  4. Cache aggressively — Redis for shared cache, LRU for in-process
  5. Compress responses — gzip reduces payload 60-80%
  6. Add database indexes — check slow query logs
  7. Monitor in production — event loop lag, memory, response times
  8. Use connection pooling — for databases, Redis, HTTP clients

Related Posts

Latest Posts