Tags: web-dev concept

The Event Loop

Date: 2026-08-16


One thread runs your JavaScript, your event handlers, and the browser’s rendering. “Asynchronous” means later on the same thread, never at the same time — and everything confusing about JS timing follows from that.


What it is

The event loop is the mechanism that repeatedly takes the next queued piece of work, runs it to completion, and then looks again.

     ┌──────────────────────────────────────┐
     │                                      │
     ▼                                      │
  take ONE task from the queue              │
     │                                      │
     ▼                                      │
  run it TO COMPLETION (no interruption)    │
     │                                      │
     ▼                                      │
  drain ALL microtasks                      │
     │                                      │
     ▼                                      │
  render, if it's time for a frame ─────────┘

The critical property is run-to-completion: once a piece of JavaScript starts, nothing else on that thread happens until it finishes. Not a click handler, not a timer, not rendering.

Async is not parallel

The most common misconception, and it costs people hours.

setTimeout(() => console.log('timer'), 0);
fetch('/api/thing').then(() => console.log('fetch'));
console.log('sync');
 
// sync
// (then, when the queue is reached) timer
// (then, whenever the network responds) fetch

setTimeout(fn, 0) does not run fn now, or concurrently. It queues it to run after the current work finishes. The delay you pass is a minimum, not a promise — if the thread is busy for 800ms, your 0ms timer fires at 800ms.

In plain terms: JavaScript never does two things at once. It does one thing, finishes, then does the next. “Async” means the browser handles the waiting elsewhere — network, timers, disk — and puts your callback in a queue for when the thread is free.

The genuinely parallel option is Web Workers, which run on separate threads and communicate by message passing.

Where the work actually happens

The thread runs JavaScript and the rendering pipeline. They compete for the same 16.7ms frame budget.

16.7ms frame

  your JS ────────────────┐
                          ├── style, layout, paint, composite
                          ┘

  if your JS takes 20ms → no frame this cycle → dropped frame → jank

This is why a heavy loop freezes the whole page: scroll stops, animations stall, clicks queue up unhandled. Nothing is broken — the thread is simply occupied. See Long Tasks and Blocking and The Rendering Pipeline.

The queues

There are two, and the ordering rule between them explains most surprising output. Task queue: timers, events, network callbacks. Microtask queue: promise callbacks, queueMicrotask, MutationObserver.

All microtasks drain after each task, before rendering. Covered properly in Tasks and Microtasks — including how an endlessly self-queueing microtask can lock the page in a way a loop of timers cannot.

Practical consequences

  • Long synchronous work blocks everything. Break it up, or move it to a worker
  • A setTimeout delay is a floor. Timing-based tests and animations built on timers will be wrong under load; use requestAnimationFrame for anything visual
  • await yields the thread. Everything between awaits runs synchronously; the rest is queued
  • Handlers queue while you’re busy. Clicks during a long task aren’t lost — they fire late, all at once, which users experience as an unresponsive page followed by a burst. That delay is exactly what Interaction to Next Paint measures
  • Rendering happens between tasks, not during one. Setting a style and then blocking for 500ms means the user sees nothing until the block ends, however early in your code the change was made

Where it comes from

Single-threaded execution isn’t an oversight — it’s what makes DOM manipulation safe. With multiple threads touching the same tree, every DOM operation would need locking, and the API would be vastly more complex. The event loop trades throughput for a model where your code never gets interrupted mid-operation.

That contrast is worth holding when you meet async in C# or PHP’s request-per-process model, both of which solve the same problem differently.