š
Intermediate
10 min readEvent 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
setTimeoutorrequestIdleCallback - 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