CloudPloy

Node.js Performance Optimization

Optimize your Node.js application performance with caching strategies, clustering, memory management, and monitoring best practices.

Performance Monitoring

Monitor your Node.js application performance to identify bottlenecks and optimization opportunities.

📊 Key Performance Metrics

Response Time Metrics

  • • Average response time
  • • 95th percentile response time
  • • Database query time
  • • External API call latency

Resource Utilization

  • • CPU usage patterns
  • • Memory consumption
  • • Event loop lag
  • • Garbage collection frequency

Built-in Performance Monitoring

// Add performance monitoring middleware
const express = require('express');
const app = express();

// Request timing middleware
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.url} - ${duration}ms`);
  });
  next();
});

// Health check with performance metrics
app.get('/health', (req, res) => {
  const memUsage = process.memoryUsage();
  const cpuUsage = process.cpuUsage();
  
  res.json({
    status: 'healthy',
    uptime: process.uptime(),
    memory: {
      rss: Math.round(memUsage.rss / 1024 / 1024) + 'MB',
      heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024) + 'MB',
      heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024) + 'MB'
    },
    cpu: {
      user: cpuUsage.user,
      system: cpuUsage.system
    }
  });
});

APM Integration

Integrate Application Performance Monitoring tools for deeper insights:

// New Relic integration
require('newrelic');

// Datadog APM
const tracer = require('dd-trace').init({
  env: process.env.NODE_ENV,
  service: 'my-nodejs-app'
});

// Custom metrics collection
const StatsD = require('node-statsd');
const client = new StatsD();

// Track custom metrics
client.increment('api.requests');
client.timing('api.response_time', responseTime);

Caching Strategies

Implement effective caching strategies to reduce response times and server load.

Memory Caching with Redis

// Redis caching configuration
const redis = require('redis');
const client = redis.createClient({
  url: process.env.REDIS_URL
});

// Cache middleware function
const cache = (duration = 300) => {
  return async (req, res, next) => {
    const key = 'cache:' + req.originalUrl;
    
    try {
      const cached = await client.get(key);
      if (cached) {
        return res.json(JSON.parse(cached));
      }
      
      const originalSend = res.json;
      res.json = function(data) {
        client.setex(key, duration, JSON.stringify(data));
        originalSend.call(this, data);
      };
      
      next();
    } catch (error) {
      next();
    }
  };
};

// Apply cache to API routes
app.get('/api/users', cache(600), getUsersHandler);

HTTP Response Caching

const express = require('express');
const app = express();

// Static asset caching
app.use('/static', express.static('public', {
  maxAge: '1y', // Cache for 1 year
  etag: true,
  lastModified: true
}));

// API response caching headers
app.get('/api/data', (req, res) => {
  // Set cache headers
  res.set({
    'Cache-Control': 'public, max-age=300', // 5 minutes
    'ETag': generateETag(data),
    'Last-Modified': data.updatedAt
  });
  
  res.json(data);
});

// Conditional requests support
app.use((req, res, next) => {
  if (req.fresh) {
    return res.sendStatus(304);
  }
  next();
});

Application-Level Caching

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

// Cache expensive operations
async function getExpensiveData(id) {
  const cacheKey = `expensive_data_${id}`;
  
  // Check cache first
  let data = cache.get(cacheKey);
  if (data) {
    return data;
  }
  
  // Fetch data if not cached
  data = await performExpensiveOperation(id);
  
  // Store in cache
  cache.set(cacheKey, data);
  
  return data;
}

// Cache invalidation
function invalidateCache(pattern) {
  const keys = cache.keys();
  keys.forEach(key => {
    if (key.includes(pattern)) {
      cache.del(key);
    }
  });
}

🚀 Caching Best Practices

  • • Use Redis for distributed caching across multiple instances
  • • Implement cache warming for critical data
  • • Set appropriate TTL values based on data freshness requirements
  • • Use cache tags for efficient invalidation
  • • Monitor cache hit rates and adjust strategies accordingly

Memory Optimization

Optimize memory usage and prevent memory leaks in your Node.js applications.

Memory Leak Detection

// Memory monitoring middleware
function memoryMonitor() {
  const usage = process.memoryUsage();
  console.log({
    rss: `${Math.round(usage.rss / 1024 / 1024)} MB`,
    heapTotal: `${Math.round(usage.heapTotal / 1024 / 1024)} MB`,
    heapUsed: `${Math.round(usage.heapUsed / 1024 / 1024)} MB`,
    external: `${Math.round(usage.external / 1024 / 1024)} MB`
  });
}

// Monitor memory every 30 seconds
setInterval(memoryMonitor, 30000);

// Heap snapshot for debugging
if (process.env.NODE_ENV === 'development') {
  const v8 = require('v8');
  
  // Generate heap snapshot
  function takeHeapSnapshot() {
    const heapSnapshot = v8.writeHeapSnapshot();
    console.log('Heap snapshot written to', heapSnapshot);
  }
  
  // Trigger on SIGUSR2
  process.on('SIGUSR2', takeHeapSnapshot);
}

Garbage Collection Tuning

# Optimize garbage collection flags
NODE_OPTIONS="--max-old-space-size=4096 --optimize-for-size"

# For high-throughput applications
NODE_OPTIONS="--max-old-space-size=8192 --max-semi-space-size=128"

# Enable GC profiling in development
NODE_OPTIONS="--trace-gc --trace-gc-verbose"

Stream Processing for Large Data

const fs = require('fs');
const csv = require('csv-parser');
const { Transform } = require('stream');

// Process large CSV files with streams
app.post('/upload/csv', (req, res) => {
  let processedRows = 0;
  
  const transformer = new Transform({
    objectMode: true,
    transform(row, encoding, callback) {
      // Process each row individually
      processRow(row)
        .then(result => {
          processedRows++;
          callback(null, result);
        })
        .catch(callback);
    }
  });
  
  req.pipe(csv())
     .pipe(transformer)
     .on('data', (data) => {
       // Stream processed data
       res.write(JSON.stringify(data) + '
');
     })
     .on('end', () => {
       res.end(`Processed ${processedRows} rows`);
     })
     .on('error', (error) => {
       res.status(500).json({ error: error.message });
     });
});

Memory-Efficient Data Processing

// Avoid loading large datasets into memory
async function processLargeDataset() {
  // Use pagination instead of loading everything
  let page = 1;
  const pageSize = 1000;
  
  while (true) {
    const data = await db.query(
      'SELECT * FROM large_table LIMIT ? OFFSET ?',
      [pageSize, (page - 1) * pageSize]
    );
    
    if (data.length === 0) break;
    
    // Process batch
    await processBatch(data);
    
    // Clear references
    data.length = 0;
    page++;
    
    // Allow garbage collection
    if (page % 10 === 0) {
      await new Promise(resolve => setImmediate(resolve));
    }
  }
}

// Use WeakMap for metadata that should be garbage collected
const metadata = new WeakMap();

function attachMetadata(obj, meta) {
  metadata.set(obj, meta);
}

function getMetadata(obj) {
  return metadata.get(obj);
}

⚠️ Common Memory Leak Sources

  • • Event listeners not properly removed
  • • Circular references in objects
  • • Global variables holding large data structures
  • • Timers and intervals not cleared
  • • Closures holding references to large objects

Database Optimization

Optimize database queries and connections for better application performance.

Connection Pooling

// PostgreSQL with connection pooling
const { Pool } = require('pg');

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20, // Maximum connections
  min: 2,  // Minimum connections
  idle: 10000, // Close idle connections after 10s
  connectionTimeoutMillis: 2000,
  statement_timeout: 30000,
  query_timeout: 30000
});

// Proper connection handling
async function queryDatabase(query, params) {
  const client = await pool.connect();
  try {
    const result = await client.query(query, params);
    return result.rows;
  } finally {
    client.release(); // Always release connection
  }
}

// Monitor pool health
setInterval(() => {
  console.log('Pool stats:', {
    total: pool.totalCount,
    idle: pool.idleCount,
    waiting: pool.waitingCount
  });
}, 60000);

Query Optimization

// Use prepared statements
const getUserByEmail = 'SELECT * FROM users WHERE email = $1';
const result = await pool.query(getUserByEmail, [email]);

// Batch operations
async function createMultipleUsers(users) {
  const query = 'INSERT INTO users (name, email, created_at) VALUES ' + 
    users.map((_, i) => '($' + (i*3+1) + ', $' + (i*3+2) + ', $' + (i*3+3) + ')').join(', ');
  
  const values = users.flatMap(user => [
    user.name, 
    user.email, 
    new Date()
  ]);
  
  return await pool.query(query, values);
}

// Use indexes effectively
const indexedQuery = `
  SELECT * FROM users 
  WHERE created_at >= $1 
  AND status = $2 
  ORDER BY created_at DESC 
  LIMIT $3
`;

// Avoid N+1 queries with joins
const usersWithPosts = await pool.query(`
  SELECT 
    u.id, u.name, u.email,
    json_agg(
      json_build_object('id', p.id, 'title', p.title)
    ) as posts
  FROM users u
  LEFT JOIN posts p ON u.id = p.user_id
  WHERE u.active = true
  GROUP BY u.id, u.name, u.email
`);

Database Caching

const Redis = require('redis');
const redis = Redis.createClient({ url: process.env.REDIS_URL });

// Cache frequently accessed data
async function getCachedUser(userId) {
  const cacheKey = `user:${userId}`;
  
  // Try cache first
  let user = await redis.get(cacheKey);
  if (user) {
    return JSON.parse(user);
  }
  
  // Fetch from database
  user = await pool.query('SELECT * FROM users WHERE id = $1', [userId]);
  
  if (user.rows[0]) {
    // Cache for 1 hour
    await redis.setex(cacheKey, 3600, JSON.stringify(user.rows[0]));
    return user.rows[0];
  }
  
  return null;
}

// Cache invalidation on updates
async function updateUser(userId, updates) {
  const result = await pool.query(
    'UPDATE users SET name = $1, email = $2, updated_at = NOW() WHERE id = $3 RETURNING *',
    [updates.name, updates.email, userId]
  );
  
  // Invalidate cache
  await redis.del(`user:${userId}`);
  
  return result.rows[0];
}

📊 Database Performance Tips

  • • Use database indexes on frequently queried columns
  • • Implement query result pagination for large datasets
  • • Use database views for complex, frequently-used queries
  • • Monitor slow query logs and optimize bottlenecks
  • • Consider read replicas for read-heavy applications

Clustering & Scaling

Utilize Node.js clustering and CloudPloy's auto-scaling features for optimal performance.

Node.js Cluster Module

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

if (cluster.isMaster) {
  console.log(`Master ${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`);
    // Restart worker
    cluster.fork();
  });
  
  // Graceful shutdown
  process.on('SIGTERM', () => {
    for (const id in cluster.workers) {
      cluster.workers[id].kill();
    }
  });
} else {
  // Worker processes
  const app = require('./app');
  const server = app.listen(process.env.PORT || 3000, () => {
    console.log(`Worker ${process.pid} started`);
  });
  
  // Graceful worker shutdown
  process.on('SIGTERM', () => {
    server.close(() => {
      process.exit(0);
    });
  });
}

PM2 Process Management

// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'my-app',
    script: './app.js',
    instances: 'max', // Use all CPU cores
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'development'
    },
    env_production: {
      NODE_ENV: 'production'
    },
    // Auto-restart configuration
    max_memory_restart: '1G',
    min_uptime: '10s',
    max_restarts: 10,
    // Monitoring
    monitoring: false,
    // Log configuration
    log_file: './logs/combined.log',
    out_file: './logs/out.log',
    error_file: './logs/error.log',
    // Advanced options
    kill_timeout: 5000,
    listen_timeout: 3000
  }]
};

Auto-Scaling Configuration

CloudPloy Auto-Scaling

Configure automatic scaling rules in your application settings:

  • • CPU Scaling: Scale up when CPU usage > 70% for 2 minutes
  • • Memory Scaling: Scale up when memory usage > 85%
  • • Response Time: Scale when average response time > 1000ms
  • • Request Rate: Scale based on requests per second thresholds
  • • Custom Metrics: Scale based on application-specific metrics

Load Testing

// Load testing with Artillery
module.exports = {
  config: {
    target: 'https://your-app.cloudploy.com',
    phases: [
      { duration: 60, arrivalRate: 10 }, // Warm up
      { duration: 120, arrivalRate: 100 }, // Ramp up
      { duration: 300, arrivalRate: 200 }, // Sustained load
      { duration: 60, arrivalRate: 10 }  // Cool down
    ]
  },
  scenarios: [
    {
      name: 'API endpoints',
      weight: 70,
      flow: [
        { get: { url: '/api/users' } },
        { think: 2 },
        { post: { 
            url: '/api/users',
            json: { name: 'Test User', email: 'test@example.com' }
          }
        }
      ]
    }
  ]
};

// Run load test
// artillery run loadtest.js

🎯 Scaling Best Practices

  • • Monitor application metrics before and after scaling
  • • Use horizontal scaling for CPU-intensive applications
  • • Implement health checks for proper load balancer routing
  • • Test auto-scaling behavior under various load patterns
  • • Set appropriate cooldown periods to prevent thrashing

Next Steps

Need Performance Optimization Help?

Our team can help you optimize your Node.js application for peak performance.