⚡ TECH BLOG
Home
Blog
Tags
About
⚡

Powered by Next.js 15 & Modern Web Tech ⚡

Back to Home

Node.js Performance Optimization: A Practical Guide

May 15, 2021
nodejsperformancebackendjavascript
Node.js Performance Optimization: A Practical Guide

Node.js Performance Optimization: A Practical Guide

Building fast Node.js applications requires understanding the event loop, memory management, and optimization techniques. This guide covers practical strategies to improve your Node.js performance.

Understanding the Event Loop

Node.js is single-threaded but highly scalable due to its event-driven architecture.

Event Loop Phases

// The event loop has several phases:
// 1. Timers - setTimeout, setInterval
// 2. Pending callbacks
// 3. Idle, prepare
// 4. Poll - I/O callbacks
// 5. Check - setImmediate
// 6. Close callbacks

// Blocking the event loop (BAD)
app.get('/compute', (req, res) => {
  const result = heavyComputation(); // Blocks event loop
  res.json(result);
});

// Non-blocking approach (GOOD)
app.get('/compute', async (req, res) => {
  const result = await new Promise(resolve => {
    setImmediate(() => resolve(heavyComputation()));
  });
  res.json(result);
});

Async Patterns for Performance

Using Worker Threads

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

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

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

function runWorker(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 stopped with code ${code}`));
    });
    worker.postMessage(data);
  });
}

Cluster Mode

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

if (cluster.isMaster) {
  const cpuCount = os.cpus().length;
  
  for (let i = 0; i < cpuCount; i++) {
    cluster.fork();
  }
  
  cluster.on('exit', (worker) => {
    console.log(`Worker ${worker.id} died. Restarting...`);
    cluster.fork();
  });
} else {
  const express = require('express');
  const app = express();
  
  app.get('/', (req, res) => {
    res.send(`Handled by worker ${process.pid}`);
  });
  
  app.listen(3000);
}

Memory Management

Avoiding Memory Leaks

// BAD: Global accumulation
const cache = {};
app.get('/data/:id', (req, res) => {
  cache[req.params.id] = largeObject; // Memory leak!
  res.json(cache[req.params.id]);
});

// GOOD: LRU Cache
const LRU = require('lru-cache');
const cache = new LRU({ max: 500, maxAge: 1000 * 60 * 60 });

app.get('/data/:id', (req, res) => {
  const cached = cache.get(req.params.id);
  if (cached) return res.json(cached);
  
  const data = fetchData(req.params.id);
  cache.set(req.params.id, data);
  res.json(data);
});

Event Listener Cleanup

// BAD: Leaking listeners
class DataEmitter extends EventEmitter {
  constructor() {
    super();
    setInterval(() => this.emit('data', getData()), 1000);
  }
}

// GOOD: Cleanup on destroy
class DataEmitter extends EventEmitter {
  constructor() {
    super();
    this.interval = setInterval(() => this.emit('data', getData()), 1000);
  }
  
  destroy() {
    clearInterval(this.interval);
    this.removeAllListeners();
  }
}

Streaming for Large Data

// BAD: Load entire file into memory
app.get('/download', async (req, res) => {
  const data = await fs.readFile('large-file.csv');
  res.send(data);
});

// GOOD: Stream the file
app.get('/download', (req, res) => {
  const stream = fs.createReadStream('large-file.csv');
  stream.pipe(res);
});

// Transform streams for processing
const { Transform } = require('stream');

const csvToJson = new Transform({
  objectMode: true,
  transform(chunk, encoding, callback) {
    const json = csvRowToJson(chunk.toString());
    callback(null, JSON.stringify(json) + '\n');
  }
});

fs.createReadStream('data.csv')
  .pipe(csvToJson)
  .pipe(fs.createWriteStream('data.jsonl'));

Database Optimization

Connection Pooling

const { Pool } = require('pg');

const pool = new Pool({
  max: 20, // Maximum connections
  min: 5,  // Minimum connections
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

app.get('/users/:id', async (req, res) => {
  const result = await pool.query('SELECT * FROM users WHERE id = $1', [req.params.id]);
  res.json(result.rows[0]);
});

Query Optimization

// BAD: N+1 queries
app.get('/posts', async (req, res) => {
  const posts = await db.query('SELECT * FROM posts');
  for (const post of posts) {
    post.author = await db.query('SELECT * FROM users WHERE id = ?', [post.author_id]);
  }
  res.json(posts);
});

// GOOD: Single query with JOIN
app.get('/posts', async (req, res) => {
  const posts = await db.query(`
    SELECT posts.*, users.name as author_name
    FROM posts
    JOIN users ON posts.author_id = users.id
  `);
  res.json(posts);
});

Caching Strategies

In-Memory Caching

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

async function getWithCache(key, fetchFn) {
  const cached = cache.get(key);
  if (cached) return cached;
  
  const data = await fetchFn();
  cache.set(key, data);
  return data;
}

app.get('/products', async (req, res) => {
  const products = await getWithCache('products', () => 
    db.query('SELECT * FROM products')
  );
  res.json(products);
});

Redis Caching

const Redis = require('ioredis');
const redis = new Redis();

async function getWithRedisCache(key, fetchFn, ttl = 3600) {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);
  
  const data = await fetchFn();
  await redis.setex(key, ttl, JSON.stringify(data));
  return data;
}

Profiling and Monitoring

Built-in Profiler

# Start with profiler
node --prof app.js

# Generate report
node --prof-process isolate-*.log > profile.txt

Clinic.js

# Install clinic
npm install -g clinic

# Analyze event loop delays
clinic doctor -- node app.js

# Analyze memory
clinic heapprofiler -- node app.js

# Flame graph
clinic flame -- node app.js

Custom Monitoring

// Monitor event loop lag
let lastCheck = Date.now();

setInterval(() => {
  const now = Date.now();
  const lag = now - lastCheck - 1000;
  console.log(`Event loop lag: ${lag}ms`);
  lastCheck = now;
}, 1000);

// Monitor memory
setInterval(() => {
  const used = process.memoryUsage();
  console.log({
    rss: `${Math.round(used.rss / 1024 / 1024)}MB`,
    heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)}MB`,
    heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)}MB`,
    external: `${Math.round(used.external / 1024 / 1024)}MB`,
  });
}, 5000);

Middleware Optimization

// BAD: Synchronous middleware
app.use((req, res, next) => {
  req.user = verifyTokenSync(req.headers.authorization);
  next();
});

// GOOD: Async middleware
app.use(async (req, res, next) => {
  try {
    req.user = await verifyToken(req.headers.authorization);
    next();
  } catch (err) {
    next(err);
  }
});

// Conditional middleware
const conditionalMiddleware = (condition, middleware) => {
  return (req, res, next) => {
    if (condition(req)) {
      middleware(req, res, next);
    } else {
      next();
    }
  };
};

Performance Checklist

  1. Use async/await instead of blocking operations
  2. Implement connection pooling for databases
  3. Use streams for large data processing
  4. Cache frequently accessed data
  5. Monitor memory usage and event loop lag
  6. Use cluster mode for CPU-intensive tasks
  7. Profile regularly with clinic or --prof
  8. Compress responses with middleware
  9. Use CDN for static assets
  10. Implement rate limiting to prevent abuse
// Essential middleware
const compression = require('compression');
const rateLimit = require('express-rate-limit');

app.use(compression());
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));

Conclusion

Optimizing Node.js performance requires understanding the event loop, proper memory management, and implementing caching strategies. Regular profiling and monitoring help identify bottlenecks before they become problems. Apply these techniques incrementally and measure their impact.

Share:

💬 Comments