DNA🌐 Web BrowserCritical Rendering Path
ðŸĢHatchlingBrowserPerformanceFundamentals

Critical Rendering Path

Understanding how the browser turns HTML, CSS, and JS into pixels is the foundation of every performance optimization. This is what separates frontend engineers from HTML writers.

Critical Rendering Path

Every performance optimization you'll ever make maps back to one question: what is the browser doing between receiving bytes and painting pixels?

The Pipeline

When the browser receives an HTML response, it goes through these stages:

  1. Parse HTML → DOM — bytes → characters → tokens → nodes → DOM tree
  2. Parse CSS → CSSOM — same process for stylesheets
  3. Combine → Render Tree — DOM + CSSOM, minus invisible elements (display: none, <head>)
  4. Layout — calculate exact position and size of every element
  5. Paint — fill in pixels: text, colors, borders, shadows, images
  6. Composite — layer management, GPU acceleration, final frame

Blocking Behavior

Not all resources are equal:

  • CSS is render-blocking — the browser won't paint until CSSOM is complete. A slow stylesheet blocks everything.
  • JS is parser-blocking — a <script> without async/defer pauses HTML parsing entirely.
  • Images are NOT render-blocking — they load asynchronously and paint when ready.
<!-- Render-blocking CSS -->
<link rel="stylesheet" href="styles.css" />
 
<!-- Parser-blocking JS -->
<script src="app.js"></script>
 
<!-- Non-blocking alternatives -->
<script src="app.js" defer></script>
<script src="analytics.js" async></script>

defer vs async

AttributeDownloadExecutionOrder guaranteed
(none)Blocks parsingImmediatelyYes
asyncParallelWhen downloadedNo
deferParallelAfter HTML parsedYes

Use defer for app scripts that depend on DOM. Use async for independent scripts like analytics.

DOM Construction Details

The parser processes HTML incrementally — it doesn't wait for the entire document:

Bytes → Characters → Tokens → Nodes → DOM

When the parser hits a <script> tag (without defer/async), it must:

  1. Pause DOM construction
  2. Download the script (if external)
  3. Execute the script
  4. Resume parsing

This is why scripts at the bottom of <body> was the old best practice — it let the DOM build first.

CSSOM and the Render-Blocking Problem

The browser builds the CSSOM by processing every stylesheet completely — CSS rules can override earlier ones, so partial CSSOM is unreliable.

<!-- Critical CSS inlined: no external request needed -->
<style>
  body { margin: 0; font-family: system-ui; }
  .hero { min-height: 100vh; }
</style>
 
<!-- Non-critical CSS loaded asynchronously -->
<link
  rel="preload"
  href="full-styles.css"
  as="style"
  onload="this.rel='stylesheet'"
/>

Inlining critical CSS eliminates the render-blocking request for above-the-fold content.

Layout (Reflow)

Layout calculates the geometry of every visible element. It's expensive because changes cascade — resizing a parent forces recalculation of all children.

Layout triggers:

  • Reading offsetWidth, offsetHeight, getBoundingClientRect()
  • Changing width, height, margin, padding, font-size
  • Adding/removing DOM elements

Forced Synchronous Layout

// Bad: read-write-read-write forces layout thrashing
elements.forEach((el) => {
  const width = el.offsetWidth;       // forces layout
  el.style.width = width * 2 + 'px';  // invalidates layout
});
 
// Good: batch reads, then batch writes
const widths = elements.map((el) => el.offsetWidth);
elements.forEach((el, i) => {
  el.style.width = widths[i] * 2 + 'px';
});

Paint and Composite

After layout, the browser paints pixels and composites layers. Some CSS properties skip layout entirely:

PropertyTriggers
width, marginLayout → Paint → Composite
color, backgroundPaint → Composite
transform, opacityComposite only

This is why animations should use transform and opacity — they bypass the expensive layout and paint stages, running on the GPU compositor thread.

/* Expensive: triggers layout on every frame */
.animate { left: 100px; transition: left 0.3s; }
 
/* Cheap: compositor only */
.animate { transform: translateX(100px); transition: transform 0.3s; }

Optimization Checklist

  1. Inline critical CSS — eliminate render-blocking stylesheet requests for above-the-fold content
  2. Defer non-critical JS — use defer or dynamic import() for non-essential scripts
  3. Minimize layout thrashing — batch DOM reads and writes, use requestAnimationFrame
  4. Promote layers wisely — will-change or transform: translateZ(0) for animated elements, but don't over-promote
  5. Preconnect to origins — <link rel="preconnect" href="https://api.example.com"> for third-party resources

Interview Signal

CRP questions test whether you understand why certain optimizations work, not just what to do. Explain the pipeline stage you're optimizing and why it matters for the specific metric (LCP, FID, CLS) the interviewer cares about.