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:
- Parse HTML â DOM â bytes â characters â tokens â nodes â DOM tree
- Parse CSS â CSSOM â same process for stylesheets
- Combine â Render Tree â DOM + CSSOM, minus invisible elements (
display: none,<head>) - Layout â calculate exact position and size of every element
- Paint â fill in pixels: text, colors, borders, shadows, images
- 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>withoutasync/deferpauses 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
| Attribute | Download | Execution | Order guaranteed |
|---|---|---|---|
| (none) | Blocks parsing | Immediately | Yes |
async | Parallel | When downloaded | No |
defer | Parallel | After HTML parsed | Yes |
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 â DOMWhen the parser hits a <script> tag (without defer/async), it must:
- Pause DOM construction
- Download the script (if external)
- Execute the script
- 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:
| Property | Triggers |
|---|---|
width, margin | Layout â Paint â Composite |
color, background | Paint â Composite |
transform, opacity | Composite 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
- Inline critical CSS â eliminate render-blocking stylesheet requests for above-the-fold content
- Defer non-critical JS â use
deferor dynamicimport()for non-essential scripts - Minimize layout thrashing â batch DOM reads and writes, use
requestAnimationFrame - Promote layers wisely â
will-changeortransform: translateZ(0)for animated elements, but don't over-promote - 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.