Concept · The browser
The event loop
One thread, a task queue and a microtask queue — why a promise callback runs before a setTimeout.
3 minmicrotasktask queuecall stackmacrotask
Your JavaScript runs on one thread. It never runs two pieces of your code at once, which is why you don't need locks. Waiting work like timers, network requests and user input happens elsewhere in the browser. When it finishes, a callback is queued, and the event loop runs queued callbacks one at a time.
The loop, simplified
repeat:
1. run one task (a script, a timer callback, an event handler) to completion
2. run ALL microtasks (promise callbacks, queueMicrotask), including new ones they add
3. if it's time, render (requestAnimationFrame callbacks, style, layout, paint)- Tasks (often called macrotasks) come from
setTimeout,setInterval, DOM events andMessageChannel. - Microtasks come from
.then/awaitcontinuations andqueueMicrotask. The whole queue drains after every task, before the next task and before rendering.
console.log('1 script')
setTimeout(() => console.log('4 timeout (task)'), 0)
Promise.resolve().then(() => console.log('3 promise (microtask)'))
console.log('2 script')
// 1 script, 2 script, 3 promise (microtask), 4 timeout (task)await is a microtask too. Everything after an await runs later, after the current task finishes.
What it explains
- A long task freezes the page. While your function runs, nothing else does: no clicks, no rendering. Anything over about 50 ms is a "long task," and long tasks are what hurt INP. Split the work into chunks with
awaiton a timer orscheduler.yield(), or move it to a Web Worker. setTimeout(fn, 0)isn't immediate. It waits for the current task and all microtasks, and browsers clamp nested timers to at least 4 ms.- Microtasks can starve rendering. A promise chain that keeps scheduling more microtasks never lets the browser paint.
- DOM changes don't paint until the task ends. Setting a style and then removing it in the same task never shows on screen. See how the browser renders.
Node.js has the same model with a few extra phases. Server languages with real threads trade this simplicity for data races you have to prevent yourself.