rgoussu@goussu: ~/library/frontend
~/library/frontend cat browser-internals.md

Browser internals

# What happens between URL and pixels — parsing, the rendering pipeline, the event loop, and the process/security model.

Conceptsaved 2026-08-08updated 2026-08-09 #frontend#browser#rendering#event-loop#internals

Overview

"What happens when you type a URL" is a cliché interview question because it threads through everything: networking, parsing, layout, paint, and JavaScript's concurrency model. For a working engineer the payoff is diagnostic — jank, layout thrashing, blocked main threads, and hydration mismatches all become legible once the pipeline underneath is concrete rather than folklore.

Key points

  • From bytes to DOM: HTML parses incrementally into the DOM; CSS into the CSSOM; <script> (without defer/async) blocks parsing — the original reason for "scripts at the bottom" and the modern reason for defer as default.

  • The rendering pipeline: style → layout (geometry) → paint (pixels into layers) → composite (GPU assembles layers). Cost descends left to right: width triggers layout+paint+composite; transform/opacity composite only — the entire basis of "animate transforms, not top/left".

    flowchart LR
        ST[Style] --> LY["Layout (geometry)"]
        LY --> PT["Paint (pixels into layers)"]
        PT --> CP["Composite (GPU assembles layers)"]
        W["Change width"] -->|"re-enters at layout: layout + paint + composite"| LY
        TR["Change transform or opacity"] -->|"composite only"| CP
    
  • The event loop: one main thread runs JS, style, layout, and paint; tasks (macrotasks) run one per loop turn, then the entire microtask queue (promises) drains before rendering gets a chance — a long task or a microtask storm freezes the page at ~16ms/frame stakes. requestAnimationFrame before paint; workers for real off-thread compute.

    flowchart TB
        MT["Run one macrotask"] --> MQ["Drain the entire microtask queue (promises)"]
        MQ --> RAF["requestAnimationFrame callbacks"]
        RAF --> RD["Rendering opportunity: style, layout, paint"]
        RD -->|"next loop turn"| MT
    
  • Layout thrashing: interleaving DOM writes with reads of layout-forcing properties (offsetHeight…) forces synchronous reflow per iteration — batch reads, then writes; it's the classic self-inflicted performance bug.

  • Process & security model: per-site processes (site isolation), the same-origin policy as the bedrock rule, CORS as its relaxation protocol (not a server shield), cookies vs. storage, and why the network tab shows preflights.

  • Modern rendering strategies sit on this: SSR streams HTML the parser can use immediately; hydration re-attaches JS to server-rendered DOM (mismatch bugs = the two renders disagreed); islands/server components shrink the JS that must hydrate at all.

  • To explore: V8's JIT tiers and hidden classes, speculative parsing & preload scanner, content-visibility, the RAIL model as budgets — and web performance for the measurement side.

Practice

  • Event-loop prediction katas (source) — write task/microtask/rAF snippets, predict the output order, then check in Loupe or DevTools; drills the loop until "why did this run first" stops being a mystery.
  • Layout-thrashing lab (source) — write a loop that interleaves DOM writes with offsetHeight reads, watch the forced reflows in the Performance panel, then batch reads/writes and measure the difference.
  • Toy browser engine (source) — build a browser in Python following Web Browser Engineering; the whole pipeline, in code you wrote yourself.

Related