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