A scroll-driven explainer · JavaScript

JavaScript has one thread. So how does it wait for timers, network and clicks without freezing? Scroll to run the code, one step at a time, and watch every function travel through the stack and the queues.

Scroll to begin
01The call stack

Code
Call stackone thread
empty
Web APIsrun by the browser, in parallel
timersfetch / networkDOM eventsworkers
Console
Event loop
Microtask queuepromises · await · queueMicrotask
Task queuetimers · events · messages
RenderrAF → style → layout → paint

Recap

The whole loop, in four rules.

Everything above follows from these. Once they click, you can predict the output of any snippet that mixes timers, promises and await.

  1. 01

    Run to completion

    A running function is never interrupted. While the stack is busy, nothing else happens: no callback, no click handler, no paint.

  2. 02

    One task per turn

    The loop takes the oldest task (a timer, an event, a message) and runs it until the stack is empty again.

  3. 03

    Then every microtask

    After each task, the whole microtask queue is drained, including microtasks queued along the way. Promises and await live here.

  4. 04

    Then, maybe, a frame

    If a frame is due, the browser runs requestAnimationFrame callbacks, then style, layout and paint. Then back to rule 02.

while (true) {
  const task = taskQueue.shift();          // rule 02: the oldest task
  run(task);                                // rule 01: until the stack is empty
  while (microtaskQueue.length) run(microtaskQueue.shift());  // rule 03
  if (frameIsDue()) { runAnimationFrames(); styleLayoutPaint(); }  // rule 04
}

Try it

Now feel it.

This ball is moved by JavaScript, one requestAnimationFrame at a time. The bars below it are the time between two painted frames. Each button keeps the main thread busy for 1.5 seconds, in a different way.

Pick an experiment. Same amount of work each time, very different results.