← Back to Tutorials Chapter 11

Performance & Scaling

Logging

Structured logging is essential for debugging and monitoring production applications.

// Install: npm install winston

const winston = require('winston');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

// Usage
logger.info('Server started', { port: 3000 });
logger.error('Database connection failed', { error: err.message });

Monitoring

// Health check endpoint
app.get('/health', (req, res) => {
  const health = {
    uptime: process.uptime(),
    status: 'OK',
    timestamp: Date.now(),
    memory: process.memoryUsage()
  };
  res.json(health);
});

// Using PM2
// Install: npm install -g pm2
// pm2 start app.js -i max
// pm2 monit
// pm2 logs
// pm2 status

Performance Optimization

// Compression example
const compression = require('compression');
app.use(compression());

// Simple in-memory cache
const cache = new Map();
async function getCachedData(key, fetchFn, ttl = 60) {
  if (cache.has(key)) {
    const { data, expiry } = cache.get(key);
    if (Date.now() < expiry) return data;
    cache.delete(key);
  }
  const data = await fetchFn();
  cache.set(key, { data, expiry: Date.now() + ttl * 1000 });
  return data;
}

Child Processes

Use child_process to run CPU-intensive operations in separate processes.

const { spawn, exec, fork } = require('child_process');

// Spawn (stream output)
const ls = spawn('ls', ['-lh', '/usr']);
ls.stdout.on('data', (data) => console.log(`stdout: ${data}`));

// Exec (buffer output)
exec('node --version', (err, stdout, stderr) => {
  if (err) return console.error(err);
  console.log('Node version:', stdout);
});

// Fork (new Node.js process)
const child = fork('worker.js');
child.send({ task: 'compute' });
child.on('message', (result) => {
  console.log('Worker result:', result);
});

Clusters

The cluster module allows you to create child processes that share the same server port, utilizing all CPU cores.

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`Master process ${process.pid} is running`);

  // Fork 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 {
  // Workers share the HTTP server
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end(`Handled by worker ${process.pid}`);
  }).listen(3000);

  console.log(`Worker ${process.pid} started`);
}

Worker Threads

For CPU-intensive JavaScript operations, use worker_threads which share the same process but run on separate threads.

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

const worker = new Worker('./worker-thread.js', {
  workerData: { iterations: 1000000 }
});

worker.on('message', (result) => {
  console.log('Result from worker:', result);
});

worker.on('error', (err) => console.error(err));

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

function compute(iterations) {
  let result = 0;
  for (let i = 0; i < iterations; i++) {
    result += Math.sqrt(i);
  }
  return result;
}

parentPort.postMessage(compute(workerData.iterations));
Exercise: Create a simple Express app with a CPU-intensive endpoint. Use PM2 to run it in cluster mode with max instances. Compare the performance before and after clustering using autocannon or wrk.