One thread, many queues
A task runs to completion, the microtask queue drains to empty, then the browser may paint. Everything confusing about async is a corollary of that sentence.
One call stack, one task at a time. When a task finishes, the engine drains the microtask queue completely, including microtasks queued by microtasks, and only then does the browser get a chance to run animation callbacks, recalculate style and layout, and paint. Then the next task begins.
Every async surprise falls out of that. A promise callback beats a zero-delay timer because it is on the other queue. A tight synchronous loop blocks paint because the renderer cannot get a turn until your task returns. A recursive microtask hangs the tab outright, where the same recursion on setTimeout stays responsive, because the microtask queue must reach empty before anything else happens.
setTimeout with zero is not zero. It means "a task, no sooner than now", clamped to about four milliseconds once nested more than five deep, throttled hard in a background tab, and late whenever the main thread is busy. Treat every timer as a floor, never as a schedule, and never as an ordering mechanism between two things you control.
You should now be able to
- State the ordering rule between tasks, microtasks and rendering
- Explain why a long synchronous loop freezes the page
Loading…