By Rahul — Google Frontend Engineer
Why JavaScript Needs an Event Loop
JavaScript is single-threaded. It can only do one thing at a time. But web apps need to handle user clicks, network responses, timers, and animations simultaneously. The event loop makes this possible by scheduling work.
Browser Event Loop
while (true) {
// 1. Execute one macrotask (or wait for one)
const task = macrotaskQueue.shift();
execute(task);
// 2. Execute ALL microtasks
while (microtaskQueue.length > 0) {
execute(microtaskQueue.shift());
}
// 3. Render (if ~16ms has passed)
if (shouldRender) {
requestAnimationFrame callbacks;
style calculation;
layout;
paint;
}
}Node.js Event Loop (libuv)
Node.js has a more complex event loop with distinct phases:
┌───────────────────────────┐
│ timers │ ← setTimeout, setInterval
├───────────────────────────┤
│ pending callbacks │ ← I/O callbacks deferred to next loop
├───────────────────────────┤
│ idle, prepare │ ← internal use
├───────────────────────────┤
│ poll │ ← retrieve new I/O events
├───────────────────────────┤
│ check │ ← setImmediate()
├───────────────────────────┤
│ close callbacks │ ← socket.on('close')
└───────────────────────────┘
// Microtasks (Promise, process.nextTick) run between EACH phaseKey Differences
process.nextTick() — Node.js Only
// Runs BEFORE any other microtask, BETWEEN phases
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
// Output: nextTick, promise
// nextTick has higher priority than Promise microtaskssetImmediate() — Node.js Only
// Runs in the "check" phase
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// Order is NOT guaranteed in the main module
// But inside an I/O callback, setImmediate always runs firstThe Classic Puzzle
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => {
console.log('3');
setTimeout(() => console.log('4'), 0);
});
Promise.resolve().then(() => console.log('5'));
console.log('6');
// Output: 1, 6, 3, 5, 2, 4
// Sync: 1, 6
// Microtasks: 3, 5
// Macrotask: 2
// Macrotask: 4 (added by microtask)Production Impact
- Long tasks: Anything blocking the main thread for 50ms+ is a "long task" that delays user interaction. Break work into smaller chunks with
setTimeout(fn, 0)orscheduler.yield() - requestAnimationFrame: Always use for visual updates — it syncs with the render cycle
- requestIdleCallback: Run non-critical work when the browser is idle
Summary
The browser event loop: macrotask → microtasks → render → repeat. Node.js has 6 phases with microtasks between each. Understanding this determines whether your UI stays responsive and your Node.js server stays fast.