Code Safari

Chapter 136·Intermediate·9 min read

The Event Loop: How Pages Stay Responsive

Why JavaScript is single-threaded yet never seems to block — the call stack, the task and microtask queues, and the event loop that ties them to rendering. How async, promises, and long tasks really work, and why a slow function freezes the whole page.

July 24, 2026

Across the last four chapters we watched the browser build the DOM, style it, lay it out, and paint it — and we kept noticing an inconvenient fact: nearly all of it, plus every line of your JavaScript, runs on one thread, the main thread. That raises an obvious question. If there's only one lane, how does a page manage to respond to a click while it's also running scripts and repainting? How is JavaScript "asynchronous" without being multi-threaded? The answer is one of the most important ideas in frontend engineering, and once it clicks, a huge amount of confusing behaviour becomes obvious. The answer is the event loop.

One thread, one call stack

JavaScript in the browser is single-threaded: there is exactly one main thread executing your code, and it does exactly one thing at a time. The structure that tracks "what it's doing right now" is the call stack. Call a function and it's pushed onto the stack; when it returns, it's popped off. If that function called another, the inner one sits on top and must finish first. The stack grows and shrinks as your program runs.

The rule that governs everything else is simple and strict: while the call stack is not empty, nothing else can happen. Not a click handler, not a timer, not a repaint. The thread is busy running whatever is on the stack, and everything else waits. This is the literal meaning of "single-threaded," and it's the reason the event loop has to exist at all.

Queues, and the loop that drains them

So the browser is off doing asynchronous work — a setTimeout counting down, a fetch waiting on the network. When each finishes, its callback doesn't barge in and interrupt whatever's running. Instead it's placed on a task queue (also called the callback or macrotask queue), a line of "things to run when you get a chance."

The event loop is the tireless coordinator that connects the two. Its entire job is this loop:

Call stack empty?
Take next task from queue
Run it to completion
Drain microtasks
Render if needed
Repeat
The event loop, one turn at a time

In words: when the call stack is empty, the event loop takes the oldest task from the queue, pushes it onto the stack, and lets it run to completion. Then — before going back for the next task — it does a couple of important things, and then repeats, forever. This is why a click handler you registered runs "later": the click is turned into a task, and it waits in the queue until the thread is free and the loop reaches it.

Microtasks: the priority lane

There's a second queue, and it explains a lot of subtle behaviour: the microtask queue. Promise callbacks (.then, and the continuation after an await), along with a few other APIs, go here rather than into the regular task queue — and microtasks have priority.

The rule is precise: after each single task finishes, the event loop drains the entire microtask queue before doing anything else — before rendering, before the next task. And microtasks can schedule more microtasks, which also run in the same drain. This is why an await tends to resume almost immediately after the awaited value is ready, while a setTimeout(fn, 0) callback — a regular task — waits behind rendering and any pending microtasks.

Where rendering fits

Now the connective tissue back to the rest of the guide. Notice where "render if needed" sits in the loop: between tasks, after microtasks are drained. The browser wants to paint roughly 60 times a second — about once every 16 milliseconds — and it can only do so when the main thread is free between tasks. Rendering is, in effect, just another thing the event loop schedules when it gets a gap.

This is the whole ballgame for smoothness. If a task runs quickly, the thread frees up, the loop gets its chance to run layout and paint, and a fresh frame appears on time. Everything feels fluid.

Long tasks: how pages freeze

But what if a task doesn't finish quickly? Suppose a click handler runs a heavy loop that takes 200 milliseconds. For that entire time, the call stack is occupied. The event loop cannot advance. Queued clicks pile up unhandled. And critically — the browser cannot render, because the thread it needs for layout and paint is busy. The page is frozen: scrolling stutters, buttons don't depress, animations stall. This is jank, and its cause is now unmistakable.

Frame budget
16ms
Smooth task
8ms
Long task (janks)
200ms
A 16ms frame budget vs. a long task hogging the thread

The frame budget is roughly 16ms. A task that fits inside it leaves room for a frame; a 200ms task blows through a dozen frame deadlines, and the user watches the page hang. Jank is not a graphics problem — it's a busy main thread. The fix is always the same shape: don't hold the thread. Break long work into smaller chunks that yield control back to the loop (so rendering and input can slip in between), move heavy computation off the main thread entirely with a Web Worker, or simply do less work. The next chapter turns this into concrete rendering-performance practice.

Where this leaves us

The event loop is the beating heart of the browser's runtime: a single thread, a call stack that must empty before anything else runs, task and microtask queues feeding it, and rendering squeezed into the gaps. "Asynchronous" JavaScript isn't extra threads — it's the discipline of not hogging the one thread you have, so the loop keeps turning and the page keeps responding.

We now have every piece of the machine: the pipeline that builds a frame, and the loop that schedules it. The final chapter puts it all to work — combining the cost hierarchy of layout, paint, and composite with the event-loop realities we just covered — to answer the practical question the whole guide has been building toward: how do you make a page genuinely fast? On to reflows, repaints, and performance.

The Event Loop: How Pages Stay Responsive | Code Safari