6.1 JavaScript is the browser's userland
The JavaScript engine — V8 in Chrome, JavaScriptCore in Safari, SpiderMonkey in Firefox — parses your source, compiles it to bytecode, and just-in-time compiles the hot paths to machine code, managing a heap of objects and a call stack of running functions, reclaiming memory with a garbage collector. That machinery is a course of its own. For this series the engine is the userland process from Module 1: the place a page's untrusted program actually executes. The interesting question is not how the engine compiles a function, but how the browser decides when to run it, given that it has exactly one main thread to run it on.
6.2 One thing at a time: run to completion
The defining rule of the model is run to completion: once a piece of JavaScript starts, it runs to its end before the browser will do anything else — no other script, no event handler, no rendering, no input. The call stack fills and empties for that one task, uninterrupted. This is a feature: you never have to worry that another thread mutated your variable halfway through a function, because there is no other thread. It is also the source of every “the page froze” experience, because if your function does not finish quickly, nothing else gets a turn.
JavaScript is single-threaded and runs to completion. Everything good and everything painful about the browser's responsiveness follows from that one sentence.
6.3 The event loop
If a task runs to completion and then the thread is free, what runs next? The event loop answers: it repeatedly takes one task from the task queue, runs it to completion, then drains the entire microtask queue, then — at most once per frame — updates the rendering. Then it goes back for the next task. That cycle, running forever, is the whole life of a page.
6.4 Tasks versus microtasks
The two queues are not interchangeable, and the difference trips up even experienced developers. A task (or macrotask) is a unit of work the loop picks up one at a time: a timer firing, a click handler, a network response callback. A microtask is a follow-up scheduled by the currently running code: a promise's .then, the continuation after an await, a queueMicrotask callback. After every task — and after the call stack empties — the loop drains the microtask queue completely before doing anything else, including before taking the next task or rendering.
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
A and D run synchronously. When the stack empties, the loop drains microtasks — C — before it will touch the task queue, so B, despite its zero delay, runs last. The practical warning hides in the word completely: because draining microtasks runs any microtasks they themselves schedule, an endless chain of promises can starve the loop and block rendering forever, even though no single function runs long. The loop is not stuck in your code; it is stuck honoring your microtasks.
6.5 Rendering is a guest on the loop
Here is where Module 5 connects. The rendering pipeline — style, layout, paint — is not a separate clock. It is step 4 of the loop, run between tasks, after microtasks, and at most once per display refresh. requestAnimationFrame is the hook into that step: a callback registered with it runs immediately before style and layout, which is exactly where visual updates belong, so they happen once per frame in sync with the pipeline rather than at some arbitrary moment that forces an extra layout. If nothing changed, or it is not yet time for a frame, the browser simply skips the rendering step and moves on.
The consequence is blunt: rendering only happens when the loop reaches step 4, and the loop only reaches step 4 when your task and its microtasks have finished. While your JavaScript runs, the page cannot repaint. The smooth-scrolling, sixty-frame-a-second page and the frozen, unresponsive one are running the identical loop; the difference is entirely whether step 2 keeps finishing in time for step 4.
6.6 The frame budget
A 60Hz display refreshes every 16.7 milliseconds, and a 120Hz display every 8.3. That interval is the entire budget for one turn of the loop that wants to produce a frame: the task, all its microtasks, the animation-frame callbacks, and style, layout, and paint must all fit. Go over, and the frame is late — the browser shows the previous frame again, and the user sees a stutter. A function that runs for forty milliseconds does not just take forty milliseconds; it costs two or three dropped frames and ignores every tap and keystroke that arrives while it runs.
6.7 Escaping the single thread
If the main thread can only do one thing at a time and rendering waits on it, heavy work has to get out of its way. There are two moves.
Yield. Break a long job into chunks and hand control back to the loop between them, so rendering and input get their turns. Historically this meant scattering work across setTimeout calls; modern platforms add purpose-built tools — scheduler.yield(), isInputPending() — that let a task voluntarily pause when something more urgent is waiting. Cooperative, but effective.
Leave the thread entirely. A Web Worker is a real operating-system thread running JavaScript in parallel with the main thread. It has no access to the DOM — it cannot touch the page — and it communicates by passing messages, but it can crunch numbers, parse data, or run a heavy algorithm without stealing a single frame from the main thread. This is genuine parallelism, and it is the same idea Module 5 already showed at work: transform and opacity animations stay smooth during a busy main thread precisely because the compositor runs them on another thread. The platform's answer to a single-threaded loop is more threads, walled off from the DOM for safety.
6.8 A cooperative scheduler
Step back to the Module 1 analogy and the picture sharpens. The event loop is a cooperative, non-preemptive scheduler: a task runs until it voluntarily finishes, and nothing can forcibly interrupt it. That is exactly the multitasking model early operating systems used before preemption — and exactly why a single misbehaving program could hang the whole machine, the same failure mode Module 1 traced in single-process browsers. The browser solved the process version with isolation; it has not made the main thread preemptive, because run-to-completion is what keeps the DOM free of data races. Web Workers are the preemptive escape hatch: real threads the operating system can schedule in parallel, kept away from shared mutable state by design. The browser is an OS that chose cooperative scheduling for its userland and quarantined the dangerous parallel work behind a message queue.
The main thread will never interrupt your code — which is a promise to you and a loaded gun pointed at the user. Finish quickly, or yield.
6.9 The lab: see the loop block
Demo Company's critical-render-path page loads a synchronous script that blocks — the loading-side version of this module's lesson. Open it with the Performance panel recording and watch the main-thread track: a long block where the script runs, no frames produced inside it, a red corner flagging it as a long task. Then run the ordering snippet from section 6.4 in the console and confirm the output is A D C B — microtasks before the next task, every time.
6.10 What you now have
The runtime model in full: one main thread, run-to-completion, and an event loop that takes one task, drains all microtasks, and renders at most once per frame. The task-versus-microtask asymmetry and its starvation risk; rendering as a guest that only runs when the loop reaches it; the 16.7-millisecond frame budget and the long task that blows past it; and the two escapes — yielding cooperatively and offloading to Web Workers, the preemptive threads kept away from the DOM. And the framing that unifies it with Module 1: the loop is a cooperative scheduler, the same design, and the same hazard, as the operating systems that came before preemption.
But notice what the loop does not know. It runs every task with equal authority — your code, the third-party CDN script from Module 4, and the malicious script you have not met yet, all on the same thread, with the same access to the same DOM. The event loop has no concept of trust. Deciding which code may do what — read which data, call which API, reach which origin — is a different subsystem entirely. Module 7 is the security model: the permission boundary that the loop, on its own, does not enforce.
The loop runs every script as an equal. The browser's hardest job is making sure equal access to the thread does not mean equal access to everything.