From Request to Render: A Deep‑Dive into Your Browser (Part 3)

Browser
Tech
Published: 20 May 2025

The raw bytes have arrived. The curtain lifts. (Read Part 2 to see how we got here)

Now the browser begins to perform: parsing HTML, styling with CSS, and breathing life into the page through JavaScript.

It all comes together element by element as the DOM forms the set, CSSOM sets the mood, and JavaScript steps into the spotlight.

Let's break down how structure, style, and interactivity take form in the browser.

  1. Inception: HTML Parsing and DOM Construction
  2. The browser doesn't wait for the full HTML response. As the first bytes arrive, parsing begins. The parser breaks down HTML into tokens (tags, attributes, text) and builds a tree called the DOM (Document Object Model).

    This parsing happens incrementally. The DOM grows as the stream progresses, which allows for progressive rendering. That's why you often see headers or navbars appear before the full page loads.

    But HTML isn't always perfect. If the parser encounters a missing or malformed tag, it doesn't crash. Browsers follow a well defined recovery strategy, injecting missing elements or closing open tags to keep the structure valid. It's part of what makes the web resilient.

    One thing that does pause the parser: scripts. When the browser encounters a <script> tag without async or defer, it stops parsing. The script is fetched, executed, and only then does HTML parsing resume. That's because scripts can manipulate the DOM or inject more HTML dynamically.

    To avoid blocking the parser, browsers support multiple loading strategies:

    • Regular scripts: Block the parser until the script finishes executing.
    • Defer scripts: Fetch in parallel, but execute after parsing is complete. They are ideal for non-blocking behaviour.
    • Async scripts: Fetch and execute as soon as possible. These don't block parsing during fetch, but do pause parsing during execution.
    • Module scripts: They are deferred automatically (unless marked async).
    <script>Scripting:HTML Parser:<script defer>Scripting:HTML Parser:<script async>Scripting:HTML Parser:<script type="module">Scripting:HTML Parser:<script type="module" async>Scripting:HTML Parser:parserfetchexecutionruntime →

  3. The Devil Wears CSS: Styling and the CSSOM
  4. While the DOM forms in one thread, another is busy parsing CSS. Stylesheets are downloaded and parsed into a structure called the CSSOM (CSS Object Model). This tree holds selectors, rules, and declarations, all tied to DOM elements later during rendering.

    CSS parsing is also done incrementally, but it blocks rendering. The browser delays visual output until all render-blocking stylesheets (<link rel="stylesheet"> tags in the <head> of your HTML) are processed. This avoids showing unstyled or partially styled content that might shift later.

    Critically, if a <script> tag appears after a stylesheet, script execution is delayed until the stylesheet is fully parsed. This is to ensure that JavaScript querying styles or changing layouts does so with accurate data.

    That's why ordering and loading attributes matter. Stylesheets go at the top. Scripts are deferred or async. Otherwise, users are left staring at a white screen longer than necessary.

    Once both DOM and CSSOM are ready, the browser constructs the render tree. This tree includes only visible elements, each with computed styles. Invisible nodes, like <head>, display: none, are excluded. The render tree is the staging area before layout and paint begin.

  5. Now You Script Me: JavaScript Execution and the Event Loop
  6. JavaScript brings interactivity to the browser. It listens for user actions, updates the DOM, and applies styles dynamically. But it also adds complexity to the browser's execution flow.

    When the browser encounters a <script>, it sends it to the JavaScript engine (like V8 in Chrome) to parse, compile, and execute. The timing of this execution depends on the script's attributes, as we saw earlier.

    JavaScript can manipulate the DOM, inject styles, or trigger layout recalculations. When used inefficiently, it can delay rendering. Long scripts block the main thread and stop the browser from responding to user input.

    To keep things responsive, the browser relies on an event loop. It coordinates everything from user clicks to network callbacks without freezing the page. Here's how it works:

    • The call stack holds the code currently being executed.
    • The task queues (yes, plural) hold tasks waiting to run.
      • Macro tasks or callback queue handles setTimeout, setInterval, fetch, and DOM events.
      • Microtasks queue handles Promise.then, queueMicrotask, and MutationObserver.

    The event loop runs one task at a time from the macro task queue. After each macro task, it drains the microtask queue before moving on.

    JavaScript is also non-preemptive. Once a task starts, it runs to completion. That's why a single expensive operation can block input handling, animation frames, and everything else.

    Code it Yourself

    Use this quick example to try in the browser console:

    📌 Note: The event loop has a lot of nuance and tricky behaviour. We'll dig into it more thoroughly in a dedicated post later.

Wrap-up

The browser has parsed the HTML, applied styles, and wired up behaviour with JavaScript. But nothing’s been drawn yet.

Next, we dive into how the browser turns it all into pixels. Layout, paint, and composite, the final act begins. Stay tuned!