๐Ÿณ IntuitiveFE
Login
โ† All concepts

Critical Rendering Path

โฑ๏ธ ~3-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

5-Year-Old Metaphor

โ€” The physical, real-world picture. No jargon.

๐Ÿ  Building a house โ€” you can't paint before measuring, and you can't move in without permits.

The construction analogy

HTML = the blueprint. Your architect's plans define every room, wall, and doorway. Without the blueprint, the contractor (browser) can't build anything. This is the DOM โ€” the structural skeleton of your page.

CSS = paint and finish specifications. You know which walls get which color, what flooring goes where. You can't apply finishes until you know what they are โ€” so CSS is render-blocking. The CSSOM is the browser's complete understanding of every style rule.

Blocking resources = waiting for permits. A blocking <script> is like a building permit that stops all construction until an inspector signs off. The entire crew (parser) waits, doing nothing, until the permit clears. async and defer are like getting a fast-track permit that doesn't pause the crew.

Render Tree = contractor's final working plan. The contractor combines the blueprint (DOM) with the finish specs (CSSOM) and marks which rooms will actually be visible. Sealed-off storage rooms (display:none) are crossed off entirely. Rooms with the lights off (visibility:hidden) are measured but not painted.

Layout = measuring every room. Before a single brushstroke, the contractor measures every dimension precisely โ€” box model, margins, padding. Change a wall (reflow) and everything must be re-measured.

Paint = applying paint and wallpaper. Pixels are filled in. A color change means repainting, but no re-measuring.

Composite = hanging the art on the GPU wall. Separate layers (CSS layers, transforms) are assembled on the GPU โ€” the fastest, most parallel step. Animations using transform happen here, completely bypassing Layout and Paint.

ASCII: the full CRP pipeline

js
1HTML bytes โ†’ [Parse] โ†’ DOM
2CSS bytes โ†’ [Parse] โ†’ CSSOM
3 DOM + CSSOM โ†’ [Render Tree] โ†’ [Layout] โ†’ [Paint] โ†’ [Composite] โ†’ ๐Ÿ–ฅ๏ธ
4ย 
5Blocking resources:
6 <script> โ†’ STOPS parser entirely (use async/defer)
7 <link rel=css> โ†’ STOPS rendering (not parsing)
8 CSS @import โ†’ SERIALIZES downloads (use <link> instead)
9ย 
10Animation cost:
11 width/height/top/left โ†’ Layout โ†’ Paint โ†’ Composite (expensive)
12 color/background โ†’ Paint โ†’ Composite (moderate)
13 transform/opacity โ†’ Composite (GPU only โ€” FREE)

The critical rule โ€” what "render-blocking" really means

A resource is render-blocking if the browser will not paint a single pixel until that resource is downloaded and processed. CSS stylesheets are always render-blocking by default. Scripts are parser-blocking AND render-blocking unless marked async or defer. Your LCP (Largest Contentful Paint) score is directly determined by how many render-blocking resources sit in the critical path.

The 5 CRP phases at a glance

๐Ÿ“„HTML Parsing

Bytes tokenized into DOM nodes. <script> blocks the parser.

๐ŸŒณDOM Construction

DOM tree built. <link rel=css> blocks rendering (not parsing).

๐ŸŽจCSSOM Construction

CSS parsed into cascade tree. @import chains are serialized.

๐ŸŒRender Tree

DOM + CSSOM merged. display:none excluded. visibility:hidden included.

๐Ÿ–Œ๏ธLayoutโ†’Paintโ†’Composite

Geometry calculated, pixels filled, GPU layers assembled.

๐ŸŽ›๏ธ

Interactive Sandbox

โ€” Move something, see it react instantly.

Phase

HTML bytes are tokenized into DOM nodes. A blocking <script> tag stops the parser entirely.

Waterfall timeline

free
Download HTML20ms

Network fetch

free
Tokenize5ms

Bytesโ†’tokens

free
Build DOM15ms

Tokensโ†’nodes

BLOCK
<script> encountered

Parser STOPS. Script must download+execute before DOM continues

free
Resume parsing8ms

Only after script completes

Step 1/5: Download HTML โ€”Network fetch
1 / 5
โš ๏ธ Gotcha: Even <script async> blocks if it finishes loading before the parser reaches it.
๐Ÿ’ก Insight: async downloads in parallel but executes immediately on load, potentially blocking. defer always executes after parsing completes, in order.
โœ… Optimization: Use async or defer on non-critical scripts. Inline critical JS.
Completed:๐Ÿ“„๐ŸŒณ๐ŸŽจ๐ŸŒ๐Ÿ–Œ๏ธ
๐ŸŽฏ

Challenge

Step through all 5 CRP phases to the final step. Note which steps block rendering โ€” that's what controls your page's time-to-first-paint.

Try it
๐ŸŽฏ

Why Should I Care?

โ€” The exact interview question + the bug it kills.

Exact interview questions

Q: What makes a resource render-blocking?

A resource is render-blocking if the browser must download and process it before it can render any content to the screen. CSS stylesheets are render-blocking by default because the browser needs the complete CSSOM to build the render tree โ€” rendering without it would cause a flash of unstyled content. Scripts are both parser-blocking and render-blocking unless tagged async or defer. The critical path is the sequence of render-blocking resources that determine Time to First Paint.

html
1<!-- Render-blocking โ€” slows first paint -->
2<link rel="stylesheet" href="styles.css" />
3<script src="app.js"></script>
4ย 
5<!-- Not render-blocking -->
6<link rel="stylesheet" href="print.css" media="print" />
7<script src="analytics.js" defer></script>
8<link rel="preload" href="font.woff2" as="font" crossorigin />

Q: Why does JavaScript block HTML parsing?

JavaScript can manipulate the DOM via document.write() and can query the DOM layout, so the browser must stop parsing HTML when it encounters a synchronous <script> tag. If parsing continued and the script tried to access a node that hasn't been parsed yet, the result would be non-deterministic. The browser has a preload scanner that speculatively downloads script src URLs even while the parser is blocked โ€” but execution still waits.

html
1<!-- Blocks parser โ€” bad for performance -->
2<script src="heavy.js"></script>
3ย 
4<!-- Downloads in parallel with parsing, executes immediately on load -->
5<script src="heavy.js" async></script>
6ย 
7<!-- Downloads in parallel, executes after DOM is complete, in order -->
8<script src="heavy.js" defer></script>
9ย 
10<!-- Inline critical JS โ€” no download, no blocking -->
11<script>window.__INITIAL_STATE__ = {...};</script>

Q: How does async vs defer differ?

Both async and defer download the script in parallel with HTML parsing. The difference is when execution happens. async executes immediately when downloaded โ€” it may interrupt parsing if the download finishes early. Scripts can execute out of order. defer always executes after parsing completes, in document order, before DOMContentLoaded. Use defer for scripts that depend on the DOM or each other.

js
1async behavior:
2 HTML: โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€[PARSE CONTINUES]โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ DOMContentLoaded
3 Script download: โ”€โ”€โ”€โ”€[downloaded]
4 Script execute: ^^^executes immediately (may interrupt parser)
5ย 
6defer behavior:
7 HTML: โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€[DONE]โ”€โ”€โ”€โ”€ DOMContentLoaded
8 Script download: โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€[downloaded]
9 Script execute: ^^^executes here (in order)
10ย 
11Rule of thumb:
12 - <script defer> for DOM-dependent scripts (use 99% of the time)
13 - <script async> for independent scripts (analytics, ads)
14 - neither only for tiny inline critical bootstraps

Production impact: real LCP numbers

Common mistake: CSS @import chains in production

Each @import is discovered only after the parent CSS is downloaded and parsed. Three levels of @import = three sequential waterfall hops before the browser has any styles at all. On a 50ms RTT connection, that's 150ms of avoidable delay before first paint โ€” often the difference between a "good" and "needs improvement" LCP score.

js
1/* โœ— Serialized โ€” catastrophic for LCP */
2/* main.css */
3@import url('typography.css'); /* discovered at T=30ms */
4/* typography.css */
5@import url('variables.css'); /* discovered at T=60ms */
6/* variables.css โ€” finally usable at T=90ms */
7ย 
8/* โœ“ Parallel โ€” all start downloading immediately */
9<link rel="stylesheet" href="main.css" />
10<link rel="stylesheet" href="typography.css" />
11<link rel="stylesheet" href="variables.css" />

Common mistake: animating layout-triggering properties

Animating top, left, width, or height triggers layout (reflow) on every frame โ€” the browser must recalculate geometry for the entire document subtree. On complex pages this can drop from 60fps to 15fps. The fix: animate transform: translate() instead, which runs on the GPU compositor thread and never touches the main thread layout engine.

js
1/* โœ— Forces Layout โ†’ Paint โ†’ Composite every frame */
2.slide-in { transition: left 300ms ease; }
3ย 
4/* โœ“ Compositor-only โ€” no Layout, no Paint, no main-thread cost */
5.slide-in { transition: transform 300ms ease; }
6/* Then move it: transform: translateX(200px) */
๐Ÿ”ฌ

The Deep Dive

โ€” Spec refs, engine internals, the minutiae.

Browser preload scanner

When the HTML parser is blocked by a synchronous script, the browser's preload scanner (also called speculative parser) continues scanning the raw HTML bytes looking for resources to fetch. It finds src attributes on scripts, href on stylesheets, and src on images โ€” and starts downloading them speculatively.

The preload scanner cannot execute JavaScript or apply CSS โ€” it only fetches. But on a high-latency connection, discovering and downloading resources speculatively while a blocking script executes can save hundreds of milliseconds.

html
1<!-- Preload scanner can discover these even while <script> blocks parser -->
2<head>
3 <script src="blocker.js"></script> <!-- blocks parser -->
4 <link rel="stylesheet" href="main.css" /> <!-- preload scanner fetches this -->
5 <script src="app.js" defer></script> <!-- preload scanner fetches this -->
6 <img src="hero.jpg" /> <!-- preload scanner fetches this -->
7</head>

Resource hints: preconnect, preload, prefetch

<link rel="preconnect">

Opens the TCP/TLS connection to a third-party origin early โ€” before the resource is actually needed. Saves 100-300ms when you know a resource will be fetched from an external CDN. Use for fonts, analytics, API endpoints.

html
1<link rel="preconnect" href="https://fonts.googleapis.com" />
2<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

<link rel="preload">

Fetches a resource with high priority and stores it โ€” but does NOT execute it. Useful for fonts (eliminate FOUT), critical CSS split from a bundle, or hero images referenced from CSS (which the preload scanner can't see).

html
1<!-- Preload LCP image โ€” browser fetches at highest priority -->
2<link rel="preload" href="/hero.webp" as="image" />
3ย 
4<!-- Preload font โ€” eliminates FOUT -->
5<link rel="preload" href="/Inter.woff2" as="font" type="font/woff2" crossorigin />

<link rel="prefetch">

Fetches a resource with low priority for likely future navigation โ€” it doesn't help current page performance at all. The browser fetches it when idle. Used for next-page bundles in SPAs.

html
1<!-- Prefetch chunk for the next route the user is likely to visit -->
2<link rel="prefetch" href="/chunks/settings-page.js" />

HTTP/2 prioritization and the CRP

HTTP/1.1 limited browsers to 6 concurrent connections per origin โ€” creating artificial queuing even for non-blocking resources. HTTP/2 multiplexes all requests over a single TCP connection with stream prioritization: the browser signals which resources it needs first, and the server sends them at higher bandwidth allocation.

Chrome's network stack uses a priority system: CSS and blocking scripts get Highest priority; preloads, fonts get High; async scripts Low; prefetch Lowest. On HTTP/2, this means critical-path resources arrive faster even when many resources are in flight simultaneously.

LCP and its relationship to the CRP

Largest Contentful Paint (LCP) โ€” the Core Web Vital that measures when the largest above-the-fold element becomes visible โ€” is directly gated by the CRP. Any render-blocking resource delays LCP. Common LCP killers:

  • Render-blocking stylesheets before the LCP image โ€” the image can't paint until CSS is parsed.
  • LCP image in CSS background-image โ€” the preload scanner can't see it; use <img> or <link rel="preload">.
  • Lazy-loading the LCP image โ€” never add loading="lazy" to the LCP candidate. Use fetchpriority="high" instead.
html
1<!-- โœ— Kills LCP โ€” lazy-loaded LCP candidate -->
2<img src="hero.webp" loading="lazy" alt="Hero" />
3ย 
4<!-- โœ“ Optimal LCP setup -->
5<link rel="preload" href="hero.webp" as="image" fetchpriority="high" />
6<img src="hero.webp" fetchpriority="high" alt="Hero" />

will-change โ€” promoting layers proactively

will-change: transform tells the browser to promote an element to its own compositor layer before any animation starts. This eliminates the one-time "layer promotion" cost at animation start โ€” useful for elements that animate frequently (menus, modals, carousels).

Overusing will-change wastes GPU memory โ€” each layer is a texture upload. Apply it only to elements that will definitely animate, and remove it after animation completes if possible.

js
1/* โœ“ Apply to elements that animate frequently */
2.dropdown-menu {
3 will-change: transform, opacity;
4}
5ย 
6/* โœ“ Or apply dynamically in JS */
7element.addEventListener('mouseenter', () => {
8 element.style.willChange = 'transform';
9});
10element.addEventListener('animationend', () => {
11 element.style.willChange = 'auto'; // release layer
12});

Quick reference table

Change typeTriggersCost
width, height, margin, padding, top, leftLayout + Paint + Composite๐Ÿ”ด Expensive
color, background, box-shadow, border-colorPaint + Composite๐ŸŸก Moderate
transform, opacityComposite only๐ŸŸข GPU-free
display:none โ†’ blockLayout + Paint + Composite๐Ÿ”ด Expensive
visibility:hidden โ†’ visiblePaint + Composite๐ŸŸก Moderate
๐ŸŽค

Interview Questions

โ€” Real questions from real interviews โ€” with answers.

HTML parse โ†’ CSSOM โ†’ render tree โ†’ layout โ†’ paint โ†’ composite. CSS and sync scripts are blocking.

Both download in parallel; async executes immediately on load, defer executes after parsing in order.

top/left triggers Layout+Paint+Composite on the main thread; transform goes straight to the compositor (GPU).

It speculatively scans HTML bytes for resources to fetch even while the parser is blocked.

It promotes the element to a GPU compositor layer before animation starts, eliminating one-time promotion cost โ€” but wastes VRAM if overused.

LCP is the largest above-the-fold element to paint. Optimize by removing render-blocking resources from the critical path and preloading the LCP image.

๐ŸŽฎ

Memory Game

โ€” Quick quiz โ€” lock the concept in long-term memory.
1/4

What is the Largest Contentful Paint (LCP) metric?