Node.js Performance Optimization

Node.js Performance Optimization: Complete Guide

Table of Contents

  1. Performance Fundamentals
  2. Event Loop Optimization
  3. Memory Management
  4. Database Optimization
  5. Caching Strategies
  6. HTTP and Network Optimization
  7. CPU-Intensive Tasks
  8. Monitoring and Profiling
  9. Deployment Optimization
  10. 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.