Node.js Performance Optimization: Complete Guide
Table of Contents
- Performance Fundamentals
- Event Loop Optimization
- Memory Management
- Database Optimization
- Caching Strategies
- HTTP and Network Optimization
- CPU-Intensive Tasks
- Monitoring and Profiling
- Deployment Optimization
- Best Practices Summary
Performance Fundamentals {#fundamentals}
Node.js performance optimization requires understanding the event-driven, non-blocking I/O model and identifying bottlenecks in your application.
Performance Metrics
const performance = require('perf_hooks').performance;
// Timing operations
function timeOperation(name, operation) {
const start = performance.now();
const result = operation();
const end = performance.now();
console.log(`${name} took ${(end - start).toFixed(2)} milliseconds`);
return result;
}
// Memory usage monitoring
function getMemoryUsage() {
const usage = process.memoryUsage();
return {
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'
};
}
// CPU usage monitoring
function getCPUUsage() {
const usage = process.cpuUsage();
return {
user: usage.user / 1000, // Convert to milliseconds
system: usage.system / 1000
};
}
// Performance monitoring middleware
function performanceMiddleware(req, res, next) {
const start = performance.now();
const startUsage = process.cpuUsage();
res.on('finish', () => {
const duration = performance.now() - start;
const cpuUsage = process.cpuUsage(startUsage);
console.log({
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: `${duration.toFixed(2)}ms`,
cpuUser: `${(cpuUsage.user / 1000).toFixed(2)}ms`,
cpuSystem: `${(cpuUsage.system / 1000).toFixed(2)}ms`,
memory: getMemoryUsage()
});
});
next();
}
Event Loop Optimization {#event-loop}
Understanding the Event Loop
// Avoid blocking the event loop
function badSyncOperation(data) {
// BAD: Synchronous operation that blocks the event loop
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += i;
}
return result;
}
function goodAsyncOperation(data, callback) {
// GOOD: Break work into chunks
let result = 0;
let i = 0;
const batchSize = 1000000;
function processChunk() {
const end = Math.min(i + batchSize, 1000000000);
for (; i < end; i++) {
result += i;
}
if (i < 1000000000) {
// Continue in next tick
setImmediate(processChunk);
} else {
callback(null, result);
}
}
processChunk();
}
// Using async/await for better control
async function processDataAsync(data) {
return new Promise((resolve) => {
let result = 0;
let i = 0;
const batchSize = 1000000;
function processChunk() {
const end = Math.min(i + batchSize, data.length);
for (; i < end; i++) {
result += data[i];
}
if (i < data.length) {
setImmediate(processChunk);
} else {
resolve(result);
}
}
processChunk();
});
}
// Worker threads for CPU-intensive tasks
const { Worker, isMainThread, parentPort } = require('worker_threads');
if (isMainThread) {
// Main thread
function heavyComputationWithWorker(data) {
return new Promise((resolve, reject) => {
const worker = new Worker(__filename);
worker.postMessage(data);
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${code}`));
}
});
});
}
} else {
// Worker thread
parentPort.on('message', (data) => {
// Perform heavy computation
let result = 0;
for (let i = 0; i < data; i++) {
result += Math.sqrt(i);
}
parentPort.postMessage(result);
});
}
// Stream processing for large datasets
const { Transform } = require('stream');
class DataProcessor extends Transform {
constructor(options = {}) {
super({ objectMode: true, ...options });
this.batchSize = options.batchSize || 100;
this.batch = [];
}
_transform(chunk, encoding, callback) {
this.batch.push(chunk);
if (this.batch.length >= this.batchSize) {
this.processBatch();
}
callback();
}
_flush(callback) {
if (this.batch.length > 0) {
this.processBatch();
}
callback();
}
processBatch() {
const processedBatch = this.batch.map(item => ({
...item,
processed: true,
timestamp: Date.now()
}));
processedBatch.forEach(item => this.push(item));
this.batch = [];
}
}
Memory Management {#memory}
Memory Leaks Prevention
// Avoiding memory leaks
class MemoryEfficientClass {
constructor() {
this.data = new Map();
this.timers = new Set();
this.listeners = new Map();
}
addData(key, value) {
// Use WeakMap for objects that can be garbage collected
if (typeof value === 'object') {
if (!this.weakData) {
this.weakData = new WeakMap();
}
this.weakData.set(value, key);
} else {
this.data.set(key, value);
}
}
addTimer(interval, callback) {
const timer = setInterval(callback, interval);
this.timers.add(timer);
return timer;
}
addListener(emitter, event, callback) {
emitter.on(event, callback);
if (!this.listeners.has(emitter)) {
this.listeners.set(emitter, new Map());
}
this.listeners.get(emitter).set(event, callback);
}
cleanup() {
// Clear all timers
this.timers.forEach(timer => clearInterval(timer));
this.timers.clear();
// Remove all listeners
this.listeners.forEach((events, emitter) => {
events.forEach((callback, event) => {
emitter.removeListener(event, callback);
});
});
this.listeners.clear();
// Clear data
this.data.clear();
}
}
// Object pooling to reduce GC pressure
class ObjectPool {
constructor(createFn, resetFn, maxSize = 100) {
this.createFn = createFn;
this.resetFn = resetFn;
this.pool = [];
this.maxSize = maxSize;
}
acquire() {
if (this.pool.length > 0) {
return this.pool.pop();
}
return this.createFn();
}
release(obj) {
if (this.pool.length < this.maxSize) {
this.resetFn(obj);
this.pool.push(obj);
}
}
}
// Example: Buffer pool for network operations
const bufferPool = new ObjectPool(
() => Buffer.allocUnsafe(8192),
(buffer) => buffer.fill(0),
50
);
function processNetworkData(data) {
const buffer = bufferPool.acquire();
try {
// Use buffer for processing
data.copy(buffer);
return processBuffer(buffer);
} finally {
bufferPool.release(buffer);
}
}
// Memory-efficient data structures
class CircularBuffer {
constructor(size) {
this.buffer = new Array(size);
this.size = size;
this.head = 0;
this.tail = 0;
this.length = 0;
}
push(item) {
this.buffer[this.tail] = item;
this.tail = (this.tail + 1) % this.size;
if (this.length < this.size) {
this.length++;
} else {
this.head = (this.head + 1) % this.size;
}
}
pop() {
if (this.length === 0) return undefined;
this.tail = (this.tail - 1 + this.size) % this.size;
const item = this.buffer[this.tail];
this.buffer[this.tail] = undefined;
this.length--;
return item;
}
shift() {
if (this.length === 0) return undefined;
const item = this.buffer[this.head];
this.buffer[this.head] = undefined;
this.head = (this.head + 1) % this.size;
this.length--;
return item;
}
}
Database Optimization {#database}
Connection Pooling
const { Pool } = require('pg');
// Optimized PostgreSQL connection pool
const pool = new Pool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
// Pool configuration
max: 20, // Maximum number of connections
idleTimeoutMillis: 30000, // Close idle connections after 30s
connectionTimeoutMillis: 2000, // Return error after 2s if can't connect
maxUses: 7500, // Close connection after 7500 uses
// Connection settings
keepAlive: true,
keepAliveInitialDelayMillis: 10000,
// Query timeout
query_timeout: 20000,
// SSL configuration
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false
});
// Optimized query functions
class DatabaseManager {
constructor(pool) {
this.pool = pool;
this.queryCache = new Map();
}
async query(text, params = []) {
const start = Date.now();
try {
const result = await this.pool.query(text, params);
const duration = Date.now() - start;
console.log('Query executed', {
text: text.substring(0, 100),
duration: `${duration}ms`,
rows: result.rowCount
});
return result;
} catch (error) {
console.error('Query error:', error);
throw error;
}
}
// Prepared statements for frequently used queries
async prepareQuery(name, text) {
this.queryCache.set(name, text);
}
async executePrepared(name, params = []) {
const text = this.queryCache.get(name);
if (!text) {
throw new Error(`Prepared query '${name}' not found`);
}
return this.query(text, params);
}
// Batch operations
async batchInsert(table, columns, data) {
if (data.length === 0) return;
const valueStrings = data.map((_, index) => {
const start = index * columns.length;
const placeholders = columns.map((_, colIndex) =>
`$${start + colIndex + 1}`
).join(', ');
return `(${placeholders})`;
}).join(', ');
const query = `
INSERT INTO ${table} (${columns.join(', ')})
VALUES ${valueStrings}
`;
const flatValues = data.flat();
return this.query(query, flatValues);
}
// Transaction management
async withTransaction(callback) {
const client = await this.pool.connect();
try {
await client.query('BEGIN');
const result = await callback(client);
await client.query('COMMIT');
return result;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
// Connection health check
async healthCheck() {
try {
const result = await this.query('SELECT 1 as healthy');
return result.rows[0].healthy === 1;
} catch (error) {
return false;
}
}
}
// Query optimization examples
const optimizedQueries = {
// Use indexes effectively
getUserByEmail: `
SELECT id, name, email, created_at
FROM users
WHERE email = $1
LIMIT 1
`,
// Pagination with OFFSET can be slow, use cursor-based pagination
getUsersPaginated: `
SELECT id, name, email, created_at
FROM users
WHERE id > $1
ORDER BY id
LIMIT $2
`,
// Join optimization
getUsersWithPosts: `
SELECT
u.id,
u.name,
u.email,
COUNT(p.id) as post_count
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
HAVING COUNT(p.id) > 0
ORDER BY post_count DESC
LIMIT $1
`,
// Batch update
updateUserLastSeen: `
UPDATE users
SET last_seen = NOW()
WHERE id = ANY($1::int[])
`
};
Caching Strategies {#caching}
Multi-level Caching
const Redis = require('ioredis');
const LRU = require('lru-cache');
class CacheManager {
constructor() {
// Level 1: In-memory cache (fastest)
this.memoryCache = new LRU({
max: 1000, // Maximum items
ttl: 5 * 60 * 1000, // 5 minutes TTL
updateAgeOnGet: true
});
// Level 2: Redis cache (shared across instances)
this.redisCache = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD,
db: 0,
retryDelayOnFailover: 100,
maxRetriesPerRequest: 3,
lazyConnect: true,
// Connection pool
family: 4,
keepAlive: true,
// Cluster configuration if using Redis Cluster
// enableOfflineQueue: false
});
this.defaultTTL = 300; // 5 minutes
}
// Get with fallback strategy
async get(key, fallbackFn = null, ttl = this.defaultTTL) {
// Try memory cache first
let value = this.memoryCache.get(key);
if (value !== undefined) {
return JSON.parse(value);
}
// Try Redis cache
try {
value = await this.redisCache.get(key);
if (value) {
// Store in memory cache for faster future access
this.memoryCache.set(key, value);
return JSON.parse(value);
}
} catch (error) {
console.error('Redis get error:', error);
}
// Use fallback function if provided
if (fallbackFn) {
const data = await fallbackFn();
await this.set(key, data, ttl);
return data;
}
return null;
}
// Set in both caches
async set(key, value, ttl = this.defaultTTL) {
const serialized = JSON.stringify(value);
// Set in memory cache
this.memoryCache.set(key, serialized);
// Set in Redis cache
try {
await this.redisCache.setex(key, ttl, serialized);
} catch (error) {
console.error('Redis set error:', error);
}
}
// Delete from both caches
async delete(key) {
this.memoryCache.delete(key);
try {
await this.redisCache.del(key);
} catch (error) {
console.error('Redis delete error:', error);
}
}
// Cache invalidation patterns
async invalidatePattern(pattern) {
// Clear memory cache (simple approach - clear all)
this.memoryCache.clear();
// Clear Redis keys matching pattern
try {
const keys = await this.redisCache.keys(pattern);
if (keys.length > 0) {
await this.redisCache.del(...keys);
}
} catch (error) {
console.error('Redis pattern delete error:', error);
}
}
// Cache warming
async warmCache(warmingFunctions) {
for (const [key, fn] of Object.entries(warmingFunctions)) {
try {
const data = await fn();
await this.set(key, data);
console.log(`Cache warmed for key: ${key}`);
} catch (error) {
console.error(`Cache warming failed for key ${key}:`, error);
}
}
}
}
// HTTP response caching middleware
function createCacheMiddleware(cacheManager) {
return function cacheMiddleware(options = {}) {
return async (req, res, next) => {
// Skip caching for non-GET requests
if (req.method !== 'GET') {
return next();
}
// Create cache key
const cacheKey = options.keyGenerator ?
options.keyGenerator(req) :
`http:${req.originalUrl}`;
try {
// Try to get cached response
const cached = await cacheManager.get(cacheKey);
if (cached) {
res.set(cached.headers);
return res.status(cached.status).send(cached.body);
}
// Capture response
const originalSend = res.send;
const originalJson = res.json;
res.send = function(body) {
// Cache successful responses
if (res.statusCode >= 200 && res.statusCode < 300) {
cacheManager.set(cacheKey, {
status: res.statusCode,
headers: res.getHeaders(),
body: body
}, options.ttl);
}
originalSend.call(this, body);
};
res.json = function(obj) {
if (res.statusCode >= 200 && res.statusCode < 300) {
cacheManager.set(cacheKey, {
status: res.statusCode,
headers: res.getHeaders(),
body: obj
}, options.ttl);
}
originalJson.call(this, obj);
};
} catch (error) {
console.error('Cache middleware error:', error);
}
next();
};
};
}
// Application-level caching strategies
class DataService {
constructor(cacheManager, dbManager) {
this.cache = cacheManager;
this.db = dbManager;
}
// Read-through cache pattern
async getUser(userId) {
return this.cache.get(
`user:${userId}`,
async () => {
const result = await this.db.query(
'SELECT * FROM users WHERE id = $1',
[userId]
);
return result.rows[0];
},
300 // 5 minutes TTL
);
}
// Write-through cache pattern
async updateUser(userId, userData) {
// Update database
const result = await this.db.query(
'UPDATE users SET name = $1, email = $2 WHERE id = $3 RETURNING *',
[userData.name, userData.email, userId]
);
// Update cache
const updatedUser = result.rows[0];
await this.cache.set(`user:${userId}`, updatedUser);
return updatedUser;
}
// Cache-aside pattern with batch operations
async getUsersBatch(userIds) {
const cached = new Map();
const missingIds = [];
// Check cache for each user
for (const id of userIds) {
const user = await this.cache.get(`user:${id}`);
if (user) {
cached.set(id, user);
} else {
missingIds.push(id);
}
}
// Fetch missing users from database
if (missingIds.length > 0) {
const result = await this.db.query(
'SELECT * FROM users WHERE id = ANY($1::int[])',
[missingIds]
);
// Cache the fetched users
for (const user of result.rows) {
cached.set(user.id, user);
await this.cache.set(`user:${user.id}`, user);
}
}
return Array.from(cached.values());
}
}
This comprehensive Node.js performance optimization guide covers the essential techniques for building high-performance applications. Remember that premature optimization is the root of all evil - always profile first, identify actual bottlenecks, then apply the appropriate optimization techniques.