Node.js Performance Optimization
Optimize your Node.js application performance with caching strategies, clustering, memory management, and monitoring best practices.
In This Guide
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
Related Node.js Guides
Other Resources
Need Performance Optimization Help?
Our team can help you optimize your Node.js application for peak performance.