šŸ”„
Intermediate
10 min read

Event Loop & Async Performance

Optimizing asynchronous operations and understanding the event loop

Understanding the JavaScript Event Loop

The event loop is the mechanism that enables JavaScript to handle asynchronous operations despite being single-threaded. Understanding how it works is crucial for writing performant code.

Event Loop Phases

  • Call Stack: Executes synchronous code
  • Microtask Queue: Promises, queueMicrotask, MutationObserver
  • Macrotask Queue: setTimeout, setInterval, I/O operations
  • Render Queue: requestAnimationFrame, browser rendering

Microtasks vs Macrotasks

Understanding the difference between microtasks and macrotasks is essential for optimization:

// āŒ Bad: Blocking the event loop with long synchronous operations
function processLargeArray(arr) {
  arr.forEach(item => {
    // Complex synchronous operation
    complexCalculation(item);
  });
}

processLargeArray(hugeArray); // Blocks UI for too long

// āœ… Good: Break work into chunks using microtasks
async function processLargeArrayOptimized(arr) {
  const chunkSize = 100;
  for (let i = 0; i < arr.length; i += chunkSize) {
    const chunk = arr.slice(i, i + chunkSize);
    chunk.forEach(item => complexCalculation(item));
    
    // Allow other tasks to run
    await new Promise(resolve => setTimeout(resolve, 0));
  }
}

// āœ… Even better: Use requestIdleCallback for non-critical work
function processWithIdleCallback(arr) {
  let index = 0;
  
  function processChunk(deadline) {
    while (deadline.timeRemaining() > 0 && index < arr.length) {
      complexCalculation(arr[index++]);
    }
    
    if (index < arr.length) {
      requestIdleCallback(processChunk);
    }
  }
  
  requestIdleCallback(processChunk);
}

Promise Optimization

Optimize Promise chains and async operations:

// āŒ Bad: Sequential Promise execution
async function fetchAllData() {
  const user = await fetchUser();
  const posts = await fetchPosts();
  const comments = await fetchComments();
  return { user, posts, comments };
}

// āœ… Good: Parallel Promise execution
async function fetchAllDataOptimized() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchComments()
  ]);
  return { user, posts, comments };
}

// āœ… Good: Handle partial failures gracefully
async function fetchWithFallback() {
  const results = await Promise.allSettled([
    fetchUser(),
    fetchPosts(),
    fetchComments()
  ]);
  
  return results.map(result => 
    result.status === 'fulfilled' ? result.value : null
  );
}

Avoiding Event Loop Blocking

// āŒ Bad: Long-running synchronous loop
function calculateFibonacci(n) {
  if (n <= 1) return n;
  return calculateFibonacci(n - 1) + calculateFibonacci(n - 2);
}

// āœ… Good: Break into smaller tasks
async function calculateFibonacciOptimized(n, memo = {}) {
  if (n <= 1) return n;
  if (memo[n]) return memo[n];
  
  // Yield to event loop periodically
  if (n % 10 === 0) {
    await new Promise(resolve => setTimeout(resolve, 0));
  }
  
  memo[n] = await calculateFibonacciOptimized(n - 1, memo) + 
             await calculateFibonacciOptimized(n - 2, memo);
  return memo[n];
}

// āœ… Best: Use Web Workers for CPU-intensive tasks
const worker = new Worker('fibonacci-worker.js');
worker.postMessage({ n: 40 });
worker.onmessage = (e) => console.log('Result:', e.data);

Microtask Scheduling

// Different ways to schedule tasks
console.log('1: Synchronous');

setTimeout(() => console.log('2: Macrotask (setTimeout)'), 0);

Promise.resolve().then(() => console.log('3: Microtask (Promise)'));

queueMicrotask(() => console.log('4: Microtask (queueMicrotask)'));

console.log('5: Synchronous');

// Output order:
// 1: Synchronous
// 5: Synchronous
// 3: Microtask (Promise)
// 4: Microtask (queueMicrotask)
// 2: Macrotask (setTimeout)

Best Practices

  • Use Promise.all() for parallel async operations
  • Break long tasks into smaller chunks with setTimeout or requestIdleCallback
  • Prioritize critical tasks using microtasks (Promises)
  • Defer non-critical work using macrotasks (setTimeout)
  • Use Web Workers for CPU-intensive calculations
  • Avoid deeply nested Promise chains - use async/await instead
  • Monitor event loop lag using performance.now()
  • Use Promise.race() for timeout patterns