Multi-process architecture, the single main thread, and the event loop that runs microtasks before every render.
A modern browser is many programs, not one. Open a tab and the operating system is running several cooperating processes, and inside each one a single main thread juggles all the work using an event loop. These two ideas — process isolation for safety, and a single-threaded loop for order — explain almost everything about how a page behaves, including why it sometimes freezes.
Rather than run everything in one process, the browser splits work across isolated processes so a crash or exploit in one can't take down or spy on the others.
Inside a renderer, the page's JavaScript and its rendering share a *single* main thread. Synchronous code runs on the call stack — a pile of function calls, where only the top item runs. Because there's just one thread, JavaScript and screen updates can never truly happen at the same instant: they take turns.
When the call stack empties, the event loop decides what runs next by a strict rule: drain every microtask, maybe render a frame, then run one macrotask — and repeat. A macrotask is work the browser queues for later: a setTimeout callback, a click handler, a network response; the loop runs *one* per turn. A microtask is higher priority — mainly a resolved promise's .then callback; the loop drains the *entire* microtask queue before moving on.
console.log('A');
setTimeout(() => console.log('D'), 0); // macrotask
Promise.resolve().then(() => console.log('C')); // microtask
console.log('B');
// prints: A, B, C, DThe synchronous lines run first, so A and B print immediately. setTimeout queues D as a macrotask and the promise queues C as a microtask. When the stack empties, the loop drains microtasks first — so C prints before the loop ever reaches the D macrotask. That ordering, A B C D, is the event loop's rule made visible.
.then and a setTimeout(fn, 0) are both queued while synchronous code runs. Which callback fires first once the stack empties?