Back to Browser Internals
Browser Internals
medium
mid

What happens during the critical rendering path when a browser loads a page?

Browser does DNS → TCP → TLS → HTTP, then parses HTML into the DOM. CSS builds the CSSOM (render-blocking). DOM + CSSOM → render tree → layout → paint → composite. Synchronous JS blocks the parser; defer/async unblock it.

7 min read·~15 min to think through

When the user hits enter on a URL, the browser executes a long chain of work. The interview-friendly summary: Network → Parse → Render. The senior detail is what blocks what — that's where every perf win comes from.

Phase 1: Network.

  1. DNS lookup. Resolve the hostname. Cached in OS, browser, router. ~20–120ms cold.
  2. TCP handshake. 3-way SYN/SYN-ACK/ACK. ~100ms RTT.
  3. TLS handshake. Certificate exchange + key agreement. 1–2 RTT (TLS 1.3 reduced this).
  4. HTTP request. Send GET, wait for first byte (TTFB). Server processes, possibly hits a CDN or origin.
  5. Response. Bytes stream back. With HTTP/2 multiplexing, more resources can come over the same connection.

Phase 2: Parse.

  1. HTML parser turns bytes into the DOM tree incrementally as bytes arrive.
  2. When the parser hits a <link rel="stylesheet">, it requests the CSS but continues parsing HTML in parallel.
  3. CSS is parsed into the CSSOM (CSS Object Model). CSS is render-blocking — the browser won't paint anything until CSSOM is built (otherwise unstyled content would flash).
  4. When the parser hits a <script>:
  • Default: parser pauses, fetches and executes the script before continuing. Worst case for performance.
  • async: fetched in parallel; executes whenever it arrives, possibly mid-parse. Order not guaranteed.
  • defer: fetched in parallel; executes after parsing is complete, in order. Best default for most scripts.
  • type="module": defer-like by default.

Phase 3: Render.

  1. Render tree combines DOM + CSSOM, dropping invisible nodes (display: none, <head>, etc.).
  2. Layout (reflow). Compute geometry of every render-tree node.
  3. Paint. Fill in pixels into layer bitmaps.
  4. Composite. GPU assembles layers and shows the frame.

Render-blocking vs parser-blocking.

  • Render-blocking = blocks the first paint. CSS by default; sync scripts; web fonts (FOIT).
  • Parser-blocking = blocks DOM construction. Sync scripts in particular — async/defer fix this.

The optimizations the CRP teaches you.

  • Inline critical CSS above the fold so the first paint doesn't wait for an external stylesheet.
  • <script defer> for app code; the parser keeps going.
  • <link rel="preload"> for late-discovered resources (font referenced from CSS, JS chunk).
  • <link rel="preconnect"> to third-party origins to skip DNS+TCP+TLS later.
  • Server push / 103 Early Hints to start asset fetches before the HTML response body arrives.
  • HTTP/2 (or HTTP/3) for multiplexing — many requests over one connection.
  • Brotli/Gzip to shrink what's on the wire.
  • HTML streaming + early flush so the browser starts parsing before the server is done generating.

Where the milliseconds go (rough budget for a fast site on a fast network):

  • DNS+TCP+TLS: 100–300ms (warmable with preconnect).
  • TTFB: 100–500ms (cacheable with CDN edge).
  • HTML download: 50–200ms.
  • CSS download + parse: 100–300ms.
  • JS download + parse + execute: 100–1000ms+ (the usual culprit).
  • Layout + paint of first frame: 50–200ms.

Web Vitals map cleanly onto CRP.

  • TTFB measures the network phase up to the first response byte.
  • FCP (First Contentful Paint) — first non-empty paint, requires HTML parsed enough + CSSOM built + render done.
  • LCP (Largest Contentful Paint) — bigger render, often gated on the hero image (preload + dimensions help).
  • CLS — caused by content shifting after first paint (images without dimensions, late-loaded fonts swapping).
  • INP — long tasks blocking the main thread after first paint (heavy JS).

Mental model in one line: the browser races to build DOM + CSSOM, the first paint waits for both, and JS controls how much of that race it slows down.

Code

html
<head>
  <meta charset="utf-8">
  <link rel="preconnect" href="https://cdn.example.com" crossorigin>
  <link rel="preload" as="image" href="/hero.webp" fetchpriority="high">
  <style>/* inlined critical CSS for above-the-fold */</style>
  <link rel="stylesheet" href="/full.css" media="print" onload="this.media='all'"> <!-- non-blocking -->
  <script src="/app.js" defer></script>
</head>
CRP-friendly head

Follow-up questions

  • Why is CSS render-blocking?
  • What's the difference between async and defer?
  • How does inlining critical CSS speed up FCP?
  • What does TTFB include?

Common mistakes

  • Putting scripts in <head> without async/defer — blocks DOM construction.
  • Treating CSS as non-blocking — it is, by default.
  • Loading too many third-party scripts synchronously — kills FCP.
  • Forgetting that web fonts can FOIT (block text rendering) without `font-display: swap`.

Performance considerations

  • First paint is gated on CSSOM — keep critical CSS small and inlined.
  • Streaming HTML + early flush gives the browser something to parse sooner.
  • Reduce JS on the critical path — every KB delays interactivity.

Edge cases

  • <link rel='preload'> without `as` — counted but downloaded again.
  • Service worker can intercept and serve from cache, short-circuiting most of phase 1.
  • HTTP/2 server push exists but is largely deprecated in favor of 103 Early Hints.

Real-world examples

  • Next.js streams HTML with React 18 to start CRP before the full page is generated.
  • Cloudflare Early Hints sends 103 with link preloads while the origin is still working.

Senior engineer discussion

Senior signal: naming the phases, what's render-blocking vs parser-blocking, and connecting CRP knobs (defer, preload, critical CSS) to specific Web Vitals.

Related questions