Isaac.

Node.js Performance Tuning

Optimize Node.js applications for speed and efficiency.

By EMEPublished: February 20, 2025
nodejsperformanceoptimizationprofiling

A Simple Analogy

Tuning Node.js is like fine-tuning an engine. Small adjustments to critical paths yield big speed improvements.


Why Performance Matters?

  • Speed: Faster response times
  • Throughput: Handle more requests
  • Cost: Run on fewer servers
  • User experience: Better engagement
  • Scalability: Support growth

Memory Profiling

// Heap snapshots
node --inspect app.js

// Or programmatically
const v8 = require('v8');
const fs = require('fs');

setInterval(() => {
  const snapshot = v8.writeHeapSnapshot();
  console.log(`Heap snapshot written to ${snapshot}`);
}, 60000);

// Monitor heap
setInterval(() => {
  const mem = process.memoryUsage();
  console.log(`Heap: ${Math.round(mem.heapUsed / 1024 / 1024)}MB`);
}, 5000);

CPU Profiling

// Node.js profiler
const profiler = require('v8-profiler-next');

profiler.startProfiling('cpu');

setTimeout(() => {
  const profile = profiler.stopProfiling('cpu');
  profile.export((err, result) => {
    if (err) return console.error(err);
    fs.writeFileSync('profile.cpuprofile', result);
    console.log('CPU profile saved');
  });
}, 30000);

// Or use clinic.js
// npm install -g clinic
// clinic doctor -- node app.js

Async Bottlenecks

// Bad: Sequential (slow)
async function fetchDataSequential() {
  const user = await db.getUser(1);
  const posts = await db.getPosts(user.id);
  const comments = await db.getComments(posts.map(p => p.id));
  return { user, posts, comments };
}

// Good: Parallel
async function fetchDataParallel() {
  const user = await db.getUser(1);
  const [posts, comments] = await Promise.all([
    db.getPosts(user.id),
    db.getComments([1, 2, 3])
  ]);
  return { user, posts, comments };
}

// Better: Use Promise.allSettled for resilience
async function fetchWithFallback() {
  const results = await Promise.allSettled([
    db.getPosts(1),
    db.getComments(1),
    cache.getData() // May fail
  ]);
  
  return results.map(r => r.status === 'fulfilled' ? r.value : null);
}

Stream Processing

// Bad: Load entire file
const fs = require('fs');

function processFile(path) {
  const data = fs.readFileSync(path, 'utf-8');
  return data.split('\n').map(line => transformLine(line));
}

// Good: Stream processing
function processFileStream(path) {
  const stream = fs.createReadStream(path, { encoding: 'utf-8' });
  
  stream.on('data', (chunk) => {
    const lines = chunk.split('\n');
    lines.forEach(line => transformLine(line));
  });
  
  stream.on('end', () => console.log('Done'));
}

// Even better: Transform stream
const { Transform } = require('stream');

const transformStream = new Transform({
  transform(chunk, encoding, callback) {
    const lines = chunk.toString().split('\n');
    const transformed = lines.map(line => transformLine(line));
    callback(null, transformed.join('\n'));
  }
});

fs.createReadStream('input.txt')
  .pipe(transformStream)
  .pipe(fs.createWriteStream('output.txt'));

Caching

const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 600 });

// Cache expensive operations
async function getUserData(userId) {
  const cached = cache.get(userId);
  if (cached) return cached;
  
  const data = await db.getUser(userId);
  cache.set(userId, data);
  
  return data;
}

// Cache with invalidation
function updateUser(userId, updates) {
  cache.del(userId);  // Invalidate cache
  return db.updateUser(userId, updates);
}

Worker Threads

const { Worker } = require('worker_threads');

// Heavy computation on worker thread
function computeOnWorker(data) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./worker.js');
    
    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', (code) => {
      if (code !== 0) reject(new Error(`Worker exit ${code}`));
    });
    
    worker.postMessage(data);
  });
}

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

parentPort.on('message', (data) => {
  const result = expensiveComputation(data);
  parentPort.postMessage(result);
});

Best Practices

  1. Profile first: Measure before optimizing
  2. Use streams: For large data
  3. Parallel execution: Use Promise.all
  4. Cache aggressively: Reduce redundant work
  5. Worker threads: Offload CPU-heavy tasks

Related Concepts

  • Clustering
  • Load balancing
  • Database query optimization
  • API performance

Summary

Node.js performance optimization requires profiling, parallelization, and strategic caching. Identify bottlenecks first, then apply targeted improvements.