HTML Parsing and the Event Loop: Where the Parser Actually Runs
In the past few articles we learned two things separately. One was about the main thread and the event loop — a scheduler that "takes a task → clears microtasks → renders when needed → loops." Another was about the HTML parser — turning bytes into the DOM via an incremental pipeline.
Separately, each makes sense. But put them side by side and an obvious question appears:
The parser looks like a straight line running on its own, while the event loop looks like a circle going round and round. What's the relation between them? Is the parser "another loop"?
This article welds the two pieces of knowledge together: the HTML parser isn't a thing that exists on its own — it is a series of tasks on the event loop. Once that clicks, every "slightly mysterious" phenomenon from earlier — progressive rendering, janky long scripts, a late DOMContentLoaded, streaming SSR — falls into place at once.
What You'll Learn
- Where the HTML parser actually "runs" (is it a separate loop or thread?)
- How network data becomes tasks on the event loop
- How parsing, scripts, rendering, and timers queue up on one main thread
- A single model that explains progressive rendering, script blocking, late
DOMContentLoaded, and streaming SSR - How the "process architecture" picture and the "event loop" picture stack together
Prerequisites
- First read the event loop article (the scheduler model):
- Then the HTML parser article (bytes to DOM):
- And the multi-process background:
Starting from a Scenario
Two pictures, both probably familiar by now.
Picture A (the event loop):
infinite loop:
1. take one task and run it
2. clear all microtasks
3. render if it's time
4. go back to step 1Picture B (the parser):
bytes → characters → tokens → DOM tree
(parses as it downloads, pauses at a <script>)Now the question: are these two pictures "two things," or "one thing described in two ways"?
The latter. The parser is not a motorized wheel spinning independently off to the side; it's work that runs inside step 1 of Picture A. In other words, the "parsing" you see is produced by the event loop running one task after another.
Core Content
1. First, Break an Intuition: the Parser Has No "Loop of Its Own"
We tend to imagine the parser as a standalone engine: give it bytes, and it whirs away until the whole document is parsed.
It isn't. Think about it: the parser needs someone to keep feeding it data, and HTML arrives asynchronously from the network. It can't "spin on its own." What actually happens is: when data arrives, the parser is woken to process a piece; when it's done, the main thread is yielded back so it can do other things. That wake/yield mechanism is the event loop.
So there is no "parsing thread" and no "parsing loop." Parsing is work scheduled on the main thread.
2. How Data Gets Into the Event Loop: The Networking Task Source
So who does the "waking"? The network does.
Recall the process-architecture article: the network process fetches the response body's bytes, which travel via IPC to the renderer process. Once in the renderer, that arriving data becomes a task on the event loop — the spec groups it under the networking task source.
network process: fetches the response body, gets bytes
│ IPC (cross-process)
▼
renderer process: bytes arrive → become a "network task" → enter the event loop
│
▼
the parser consumes this batch of bytes within that taskAn everyday analogy: the main thread is the only cashier, and it doesn't run out to the door to fetch packages. The packages (network data) are tasks "delivered to the counter" one at a time; the cashier handles each one, and the moment it's done, it can do something else.
3. Parsing Is Chopped into Tasks: A Chunk Arrives, Parses, Pauses
So "parse one whole HTML document" is, in reality, chopped into many tasks:
[task] chunk 1 bytes arrive → parse <head>...</head>, a few nodes
[task] chunk 2 bytes arrive → keep building the tree
[task] chunk 3 bytes arrive → hit <script> → parsing pauses
[task] script download/execute
[task] chunk 4 bytes arrive → resume, keep building
...
[task] last chunk → reach EOF, DOM completeOne detail is worth making explicit: how much gets parsed in a single task isn't fixed. It depends on how many bytes were "delivered to the counter" this time, and whether the parser gets interrupted by a script. In other words, the "chunking" is decided by the network's delivery granularity and pauses, not by some rhythm the parser sets for itself.
4. Rendering Slots In Between Tasks: the Real Source of Progressive Rendering
Now drop step 3 of the event loop (rendering) onto that timeline, and you get the real source of progressive rendering:
[task] parse chunk 1 → DOM grows an <h1>...
[render] content + known CSS ready → paint a frame ← first paint appears!
[task] parse chunk 2
[render] paint another frame
..."The DOM isn't complete yet it can already paint" isn't a special parser superpower; it's simply because parsing happens task by task, and the render step naturally slots between those tasks. This is the mechanical root of "rendering doesn't wait for a complete DOM."
5. Scripts: Parsing Pauses → Script Task → Resume
What happens at a plain <script> looks identical through the task lens:
[task] parse to <script> → parser pauses, hands control back to the event loop
[task] download (if external) + execute the script
[task] script finishes → parser resumes, keeps building the treeSo "a script blocks parsing" really means: the chain of parsing tasks is blocked in the middle by a script task. And script tasks and parsing tasks live on the same main thread, so neither can preempt the other — which is why a long script stalls all the later parsing and rendering.
6. Microtasks Are In On It Too
Step 2 of the event loop (clearing microtasks) applies during parsing as well. For example, MutationObserver callbacks fired during parsing are microtasks, drained "after the current parsing task ends and before the next task starts" — that is, before rendering. This explains a counterintuitive point: microtasks can jump the queue ahead of rendering, so an infinite microtask chain can starve the page (same effect as while(true)).
7. DOMContentLoaded / load Are Tasks Too
When parsing reaches EOF, DOMContentLoaded fires. It isn't "shouted directly by the parser"; it's a task dispatched by the event loop:
[task] parse to EOF → run the "end" steps → queue DOMContentLoaded as a new task
[task] dispatch the DOMContentLoaded eventload works the same way (dispatched once all resources finish). Since they're tasks, they have to queue — which explains why DCL sometimes looks "a beat late": it isn't slow, it's just that tasks ahead of it haven't finished.
8. Verifying with Streaming SSR
A little background first: streaming SSR means the server doesn't send the whole HTML at once — it keeps the connection open and pushes the page in chunks (Next.js's App Router + Suspense is a typical example). Run it through this task model and it's remarkably smooth:
streaming SSR: the server keeps the connection open, pushing HTML in chunks
[task] chunk 1 arrives → parse, build DOM
[render] first paint appears ← early
...
[task] later chunks arrive → keep parsing
(the stream never ends → there is never an "EOF task")
...
[task] the stream finally ends → EOF → dispatch DOMContentLoaded ← lateFirst paint early, DCL late is exactly "a render slots between tasks" happening early, while "the EOF task" refuses to come. The two are decoupled by streaming — no coincidence, but a conclusion the event loop model hands you.
9. Processes + Event Loop: Stack the Two Pictures
Finally, stack the two pictures from previous articles, and you hold the complete mental model:
[process layer]
network process ──IPC──► renderer process (main thread)
│
├─ parsing task (build DOM / CSSOM)
├─ script task (run JS)
├─ network task (feed the parser)
└─ timers / events / ...
[event loop layer]
main thread = the event loop:
take a task → clear microtasks → render if needed → loopProcesses decide "who works on the main thread"; the event loop decides "who the main thread works on next." Parsing, scripts, and rendering are all products of these two layers cooperating.
Deeper Understanding (Common Misconceptions)
- "HTML parsing is an independent loop/thread." No. It's a series of tasks on the main thread, scheduled by the event loop.
- "Parsing only moves once a big chunk of data arrives." No. Data is processed as a task the moment it arrives; the granularity isn't fixed.
- "DOMContentLoaded is fired directly by the parser." It's a task dispatched by the event loop, so it queues too.
- "Rendering runs in parallel with parsing." No. Same main thread — rendering can only slot between tasks.
- "Streaming SSR is just slow loading." Inaccurate. The first paint is actually early (progressive rendering); what's late is the document-completion events.
Hands-On Practice
-
See "parsing tasks" and "frames" alternate in the Performance panel
Record a page load. You'll see
Parse HTMLin segments, interspersed withFrame/Layoutblocks — that's "rendering slots between tasks." -
Watch progressive parsing and DCL with a slow response
Start a local server that dribbles out HTML over several seconds. Observe: content appears early (progressive rendering), while
DOMContentLoadedcomes after. -
Watch
document.readyStateLog
document.readyStatein the console and watch it goloading→interactive(just before DCL) →complete. -
Long task vs. chunked task
Compare running a 3-second heavy loop directly with splitting it into small pieces via
setTimeout, and compare the first-paint and DCL timings.
Summary & Keywords
- The parser isn't an independent loop/thread; it's a series of tasks on the event loop.
- Network data travels via IPC from the network process to the renderer process, then feeds the parser as network tasks.
- Parsing is chopped into multiple tasks, and "rendering" slots between them — the source of progressive rendering.
- A script blocks parsing = the parsing-task chain is blocked by a long script task; DCL/load are tasks too, so they queue.
- This one model explains progressive rendering, script jank, late DCL, and streaming SSR.
- Processes decide "who works"; the event loop decides "who's next."
Keywords: event loop, task (macrotask), microtask, networking task source, HTML parsing, progressive rendering, DOMContentLoaded, load, streaming SSR, IPC, document.readyState
Further Reading and Next Article Preview
At this point, how parsing, scripts, and rendering queue up on one main thread is fully connected.
Next, we can finally take the last step down: how does the browser turn DOM + CSSOM into the pixels you see? That's the rendering pipeline — style computation (Style) → layout (Layout) → paint (Paint) → composite (Composite) — and why "changing a width" and "changing a transform" cost wildly different amounts.
Earlier articles:
