`n

Event Loop Optimization - Complete Guide

Published: September 25, 2024 | Reading time: 22 minutes

Event Loop Overview

The Node.js event loop is the core of asynchronous operations:

Event Loop Benefits
# Event Loop Benefits
- Non-blocking I/O operations
- High concurrency
- Efficient resource usage
- Scalable applications
- Responsive user experience
- Better performance
- Cost-effective scaling

Event Loop Phases

Understanding Event Loop Phases

Event Loop Phases
# Event Loop Phases

# 1. Timer Phase
setTimeout(() => {
  console.log('Timer callback');
}, 0);

setInterval(() => {
  console.log('Interval callback');
}, 1000);

# 2. Pending Callbacks Phase
const fs = require('fs');

fs.readFile('file.txt', (err, data) => {
  if (err) {
    console.error('Error:', err);
    return;
  }
  console.log('File read:', data);
});

# 3. Idle, Prepare Phase
// Internal use only

# 4. Poll Phase
const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello World');
});

server.listen(3000, () => {
  console.log('Server running on port 3000');
});

# 5. Check Phase
setImmediate(() => {
  console.log('setImmediate callback');
});

# 6. Close Callbacks Phase
const server = http.createServer();

server.on('close', () => {
  console.log('Server closed');
});

# 7. Event Loop Monitoring
const { performance, PerformanceObserver } = require('perf_hooks');

const obs = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    console.log(`${entry.name}: ${entry.duration}ms`);
  });
});

obs.observe({ entryTypes: ['measure'] });

# 8. Event Loop Lag Detection
function measureEventLoopLag() {
  const start = process.hrtime.bigint();
  
  setImmediate(() => {
    const lag = Number(process.hrtime.bigint() - start) / 1000000;
    console.log(`Event loop lag: ${lag.toFixed(2)}ms`);
  });
}

# 9. Event Loop Utilization
const { performance } = require('perf_hooks');

function measureEventLoopUtilization() {
  const start = performance.now();
  
  setImmediate(() => {
    const end = performance.now();
    const utilization = (end - start) / 1000;
    console.log(`Event loop utilization: ${utilization.toFixed(2)}ms`);
  });
}

# 10. Event Loop Health Check
class EventLoopMonitor {
  constructor() {
    this.lagThreshold = 10; // ms
    this.utilizationThreshold = 50; // ms
    this.samples = [];
    this.maxSamples = 100;
  }
  
  measureLag() {
    const start = process.hrtime.bigint();
    
    setImmediate(() => {
      const lag = Number(process.hrtime.bigint() - start) / 1000000;
      this.samples.push(lag);
      
      if (this.samples.length > this.maxSamples) {
        this.samples.shift();
      }
      
      if (lag > this.lagThreshold) {
        console.warn(`High event loop lag detected: ${lag.toFixed(2)}ms`);
      }
    });
  }
  
  getAverageLag() {
    if (this.samples.length === 0) return 0;
    return this.samples.reduce((sum, lag) => sum + lag, 0) / this.samples.length;
  }
  
  getMaxLag() {
    return Math.max(...this.samples);
  }
}

Blocking Prevention

Avoiding Event Loop Blocking

Blocking Prevention
# Blocking Prevention

# 1. CPU-Intensive Tasks
// Bad - blocks event loop
function fibonacci(n) {
  if (n < 2) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// Good - use worker threads
const { Worker, isMainThread, parentPort } = require('worker_threads');

if (isMainThread) {
  function fibonacciAsync(n) {
    return new Promise((resolve, reject) => {
      const worker = new Worker(__filename);
      worker.postMessage(n);
      worker.on('message', resolve);
      worker.on('error', reject);
    });
  }
} else {
  parentPort.on('message', (n) => {
    const result = fibonacci(n);
    parentPort.postMessage(result);
  });
}

# 2. Large File Processing
// Bad - blocks event loop
const fs = require('fs');

function processLargeFile() {
  const data = fs.readFileSync('large-file.txt');
  // Process data synchronously
  return data.toString().toUpperCase();
}

// Good - use streams
function processLargeFileAsync() {
  const fs = require('fs');
  const { Transform } = require('stream');
  
  const transform = new Transform({
    transform(chunk, encoding, callback) {
      callback(null, chunk.toString().toUpperCase());
    }
  });
  
  return fs.createReadStream('large-file.txt')
    .pipe(transform);
}

# 3. Database Operations
// Bad - synchronous database calls
function getUsersSync() {
  const users = database.query('SELECT * FROM users');
  return users;
}

// Good - asynchronous database calls
async function getUsersAsync() {
  try {
    const users = await database.query('SELECT * FROM users');
    return users;
  } catch (error) {
    console.error('Database error:', error);
    throw error;
  }
}

# 4. JSON Parsing
// Bad - large JSON parsing
function parseLargeJSON() {
  const data = JSON.parse(fs.readFileSync('large-data.json'));
  return data;
}

// Good - streaming JSON parser
const JSONStream = require('JSONStream');

function parseLargeJSONAsync() {
  return fs.createReadStream('large-data.json')
    .pipe(JSONStream.parse('*'));
}

# 5. Image Processing
// Bad - synchronous image processing
const sharp = require('sharp');

function processImageSync() {
  return sharp('input.jpg')
    .resize(800, 600)
    .jpeg()
    .toBuffer();
}

// Good - asynchronous image processing
async function processImageAsync() {
  try {
    const buffer = await sharp('input.jpg')
      .resize(800, 600)
      .jpeg()
      .toBuffer();
    return buffer;
  } catch (error) {
    console.error('Image processing error:', error);
    throw error;
  }
}

# 6. Encryption/Decryption
// Bad - synchronous encryption
const crypto = require('crypto');

function encryptSync(data) {
  const cipher = crypto.createCipher('aes192', 'password');
  let encrypted = cipher.update(data, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  return encrypted;
}

// Good - asynchronous encryption
async function encryptAsync(data) {
  return new Promise((resolve, reject) => {
    const cipher = crypto.createCipher('aes192', 'password');
    let encrypted = '';
    
    cipher.on('data', (chunk) => {
      encrypted += chunk.toString('hex');
    });
    
    cipher.on('end', () => {
      resolve(encrypted);
    });
    
    cipher.on('error', reject);
    
    cipher.write(data);
    cipher.end();
  });
}

# 7. Network Requests
// Bad - synchronous HTTP requests
const https = require('https');

function makeRequestSync(url) {
  const data = https.get(url, (res) => {
    let data = '';
    res.on('data', (chunk) => {
      data += chunk;
    });
    res.on('end', () => {
      return data;
    });
  });
}

// Good - asynchronous HTTP requests
async function makeRequestAsync(url) {
  try {
    const response = await fetch(url);
    const data = await response.text();
    return data;
  } catch (error) {
    console.error('Request error:', error);
    throw error;
  }
}

# 8. File System Operations
// Bad - synchronous file operations
function readFilesSync() {
  const files = fs.readdirSync('.');
  const contents = files.map(file => fs.readFileSync(file));
  return contents;
}

// Good - asynchronous file operations
async function readFilesAsync() {
  try {
    const files = await fs.promises.readdir('.');
    const contents = await Promise.all(
      files.map(file => fs.promises.readFile(file))
    );
    return contents;
  } catch (error) {
    console.error('File operation error:', error);
    throw error;
  }
}

# 9. Data Processing
// Bad - synchronous data processing
function processDataSync(data) {
  return data.map(item => {
    // CPU-intensive processing
    return item * 2;
  });
}

// Good - chunked processing
async function processDataAsync(data) {
  const chunkSize = 1000;
  const chunks = [];
  
  for (let i = 0; i < data.length; i += chunkSize) {
    chunks.push(data.slice(i, i + chunkSize));
  }
  
  const results = [];
  for (const chunk of chunks) {
    const processed = await processChunk(chunk);
    results.push(...processed);
    
    // Yield control to event loop
    await new Promise(resolve => setImmediate(resolve));
  }
  
  return results;
}

# 10. Event Loop Yielding
function yieldToEventLoop() {
  return new Promise(resolve => setImmediate(resolve));
}

async function longRunningTask() {
  for (let i = 0; i < 1000000; i++) {
    // Do some work
    if (i % 1000 === 0) {
      await yieldToEventLoop();
    }
  }
}

Performance Optimization

Event Loop Optimization Techniques

Optimization Techniques

  • Use worker threads
  • Implement streaming
  • Chunk large operations
  • Use async/await
  • Implement caching
  • Monitor event loop lag
  • Use connection pooling

Common Blocking Operations

  • Synchronous file I/O
  • CPU-intensive calculations
  • Large JSON parsing
  • Synchronous database calls
  • Image processing
  • Encryption operations
  • Network requests

Monitoring and Debugging

Event Loop Monitoring

Event Loop Monitoring
# Event Loop Monitoring

# 1. Event Loop Lag Monitoring
class EventLoopLagMonitor {
  constructor() {
    this.lagThreshold = 10; // ms
    this.samples = [];
    this.maxSamples = 1000;
  }
  
  start() {
    setInterval(() => {
      this.measureLag();
    }, 1000);
  }
  
  measureLag() {
    const start = process.hrtime.bigint();
    
    setImmediate(() => {
      const lag = Number(process.hrtime.bigint() - start) / 1000000;
      this.samples.push(lag);
      
      if (this.samples.length > this.maxSamples) {
        this.samples.shift();
      }
      
      if (lag > this.lagThreshold) {
        console.warn(`High event loop lag: ${lag.toFixed(2)}ms`);
      }
    });
  }
  
  getStats() {
    if (this.samples.length === 0) return null;
    
    const sorted = [...this.samples].sort((a, b) => a - b);
    const avg = this.samples.reduce((sum, lag) => sum + lag, 0) / this.samples.length;
    const p95 = sorted[Math.floor(sorted.length * 0.95)];
    const p99 = sorted[Math.floor(sorted.length * 0.99)];
    
    return {
      average: avg,
      p95,
      p99,
      max: Math.max(...this.samples),
      min: Math.min(...this.samples)
    };
  }
}

# 2. Event Loop Utilization Monitoring
const { performance } = require('perf_hooks');

class EventLoopUtilizationMonitor {
  constructor() {
    this.utilizationThreshold = 50; // ms
    this.samples = [];
    this.maxSamples = 1000;
  }
  
  start() {
    setInterval(() => {
      this.measureUtilization();
    }, 1000);
  }
  
  measureUtilization() {
    const start = performance.now();
    
    setImmediate(() => {
      const utilization = performance.now() - start;
      this.samples.push(utilization);
      
      if (this.samples.length > this.maxSamples) {
        this.samples.shift();
      }
      
      if (utilization > this.utilizationThreshold) {
        console.warn(`High event loop utilization: ${utilization.toFixed(2)}ms`);
      }
    });
  }
  
  getStats() {
    if (this.samples.length === 0) return null;
    
    const avg = this.samples.reduce((sum, util) => sum + util, 0) / this.samples.length;
    const max = Math.max(...this.samples);
    const min = Math.min(...this.samples);
    
    return { average: avg, max, min };
  }
}

# 3. Event Loop Health Check
class EventLoopHealthCheck {
  constructor() {
    this.lagMonitor = new EventLoopLagMonitor();
    this.utilizationMonitor = new EventLoopUtilizationMonitor();
  }
  
  start() {
    this.lagMonitor.start();
    this.utilizationMonitor.start();
  }
  
  getHealthStatus() {
    const lagStats = this.lagMonitor.getStats();
    const utilStats = this.utilizationMonitor.getStats();
    
    if (!lagStats || !utilStats) {
      return { status: 'unknown', message: 'Insufficient data' };
    }
    
    if (lagStats.p95 > 20 || utilStats.average > 100) {
      return { status: 'critical', message: 'Event loop performance degraded' };
    }
    
    if (lagStats.p95 > 10 || utilStats.average > 50) {
      return { status: 'warning', message: 'Event loop performance suboptimal' };
    }
    
    return { status: 'healthy', message: 'Event loop performance good' };
  }
}

# 4. Event Loop Profiling
const { PerformanceObserver } = require('perf_hooks');

class EventLoopProfiler {
  constructor() {
    this.observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        console.log(`${entry.name}: ${entry.duration}ms`);
      });
    });
    
    this.observer.observe({ entryTypes: ['measure'] });
  }
  
  profileFunction(name, fn) {
    performance.mark(`${name}-start`);
    
    const result = fn();
    
    performance.mark(`${name}-end`);
    performance.measure(name, `${name}-start`, `${name}-end`);
    
    return result;
  }
  
  async profileAsyncFunction(name, fn) {
    performance.mark(`${name}-start`);
    
    const result = await fn();
    
    performance.mark(`${name}-end`);
    performance.measure(name, `${name}-start`, `${name}-end`);
    
    return result;
  }
}

# 5. Event Loop Metrics Collection
class EventLoopMetrics {
  constructor() {
    this.metrics = {
      lag: [],
      utilization: [],
      timestamps: []
    };
  }
  
  collect() {
    const start = process.hrtime.bigint();
    const perfStart = performance.now();
    
    setImmediate(() => {
      const lag = Number(process.hrtime.bigint() - start) / 1000000;
      const utilization = performance.now() - perfStart;
      const timestamp = Date.now();
      
      this.metrics.lag.push(lag);
      this.metrics.utilization.push(utilization);
      this.metrics.timestamps.push(timestamp);
      
      // Keep only last 1000 samples
      if (this.metrics.lag.length > 1000) {
        this.metrics.lag.shift();
        this.metrics.utilization.shift();
        this.metrics.timestamps.shift();
      }
    });
  }
  
  getMetrics() {
    return {
      lag: {
        average: this.getAverage(this.metrics.lag),
        max: Math.max(...this.metrics.lag),
        min: Math.min(...this.metrics.lag),
        p95: this.getPercentile(this.metrics.lag, 95),
        p99: this.getPercentile(this.metrics.lag, 99)
      },
      utilization: {
        average: this.getAverage(this.metrics.utilization),
        max: Math.max(...this.metrics.utilization),
        min: Math.min(...this.metrics.utilization)
      }
    };
  }
  
  getAverage(values) {
    return values.reduce((sum, val) => sum + val, 0) / values.length;
  }
  
  getPercentile(values, percentile) {
    const sorted = [...values].sort((a, b) => a - b);
    const index = Math.floor(sorted.length * percentile / 100);
    return sorted[index];
  }
}

Summary

Event loop optimization involves several key components:

  • Event Loop Phases: Understanding timer, poll, check, and close phases
  • Blocking Prevention: Avoiding CPU-intensive and synchronous operations
  • Performance Optimization: Using worker threads, streaming, and async patterns
  • Monitoring: Real-time event loop lag and utilization tracking

Need More Help?

Struggling with event loop optimization or need help preventing blocking operations? Our Node.js experts can help you optimize your application's performance.

Get Event Loop Help