10 min read
Part of Series: Frontend Architecture & Patterns

Critical Rendering Path

Learn how browsers convert HTML, CSS, and JavaScript into pixels, understanding DOM construction, CSSOM building, layout calculations, and paint operations for optimal web performance.

The Critical Rendering Path is the browser’s process of converting HTML, CSS, and JavaScript into a visual representation. This process involves multiple stages where the browser constructs data structures, calculates styles, determines layout, and finally paints pixels to the screen.

MetricWhat CRP Stage Influences It MostWhat Causes Blocking
First Contentful Paint (FCP)HTML → DOM, CSS → CSSOMRender-blocking CSS
Largest Contentful Paint (LCP)Layout → PaintHeavy images, slow resource fetch
Interaction to Next Paint (INP)Style-Calc, Layout, Paint, CompositeLong tasks, forced reflows
Frame Budget (≈16 ms)Style → Layout → Paint → CompositeExpensive paints, too many layers

The modern CRP consists of six distinct stages. Each stage must complete before the next can begin, creating potential bottlenecks in the rendering process.

The browser begins by parsing the raw HTML bytes it receives from the network. This process involves:

  • Conversion: Translating bytes into characters using the specified encoding (e.g., UTF-8).
  • Tokenizing: Breaking the character stream into tokens (e.g., <html>, <body>, text nodes) as per the HTML5 standard.
  • Lexing: Converting tokens into nodes with properties and rules.
  • DOM Tree Construction: Linking nodes into a tree structure that represents the document’s structure and parent-child relationships.

Incremental Parsing: The browser does not wait for the entire HTML document to download before starting to build the DOM. It parses and builds incrementally, which allows it to discover resources (like CSS and JS) early and start fetching them sooner.

<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
<title>Critical Path</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
</body>
</html>

DOM Construction Example

Visual representation of DOM tree construction from HTML parsing showing parent-child relationships

As the browser encounters <link rel="stylesheet"> or <style> tags, it fetches and parses CSS into the CSS Object Model (CSSOM):

  • CSSOM: A tree of all CSS selectors and their computed properties.
  • Cascading: Later CSS rules can override earlier ones, so the browser must have the complete picture before rendering.
  • NOT Parser-Blocking: CSS is not parser-blocking—the HTML parser continues to process the document while CSS is being fetched.
  • Render-Blocking: CSS is render-blocking by default. The browser must download and parse all CSS before it can safely render any content. This prevents Flash of Unstyled Content (FOUC) and ensures correct cascading.
  • JS-Blocking: If a <script> tag is encountered that needs to access computed styles (e.g., via getComputedStyle()), the browser must wait for all CSS to be loaded and parsed before executing that script. This is because the script may depend on the final computed styles, which are only available after the CSSOM is complete.

Example: If a script tries to read an element’s color or size, the browser must ensure all CSS is applied before running the script, otherwise the script could get incorrect or incomplete style information.

Summary: CSS blocks rendering and can block JS execution, but does not block the HTML parser itself.

Sample CSS:

body {
font-size: 16px;
}
p {
font-weight: bold;
}
span {
color: red;
}
p span {
display: none;
}
img {
float: right;
}

CSSOM tree structure showing how CSS rules are organized and cascaded during parsing

Non-Render-Blocking CSS:

  • Use the media attribute (e.g., media="print") to load non-critical CSS without blocking rendering.
  • Chrome 105+ supports blocking=render for explicit control.

JavaScript can be loaded in several modes, each affecting how and when scripts are executed relative to HTML parsing and CSS loading.

  • <script src="main.js"></script>
  • Blocks the HTML parser until the script is downloaded and executed.
  • Order is preserved for multiple scripts.
  • JS execution is also blocked on CSS if the script may access computed styles (see above).
  • <script src="main.js" async></script>
  • Does not block the HTML parser; script is fetched in parallel.
  • Executes as soon as it is downloaded, possibly before or after DOM is parsed.
  • Order is NOT preserved for multiple async scripts.
  • Still blocked on CSS if the script accesses computed styles.
  • <script src="main.js" defer></script>
  • Does not block the HTML parser; script is fetched in parallel.
  • Executes after the DOM is fully parsed, in the order they appear in the document.
  • Still blocked on CSS if the script accesses computed styles.
  • <script type="module" src="main.js"></script>
  • Deferred by default (like defer).
  • Supports import/export syntax and top-level await.
  • Executed after the DOM is parsed and after all dependencies are loaded.
  • Order is not guaranteed for multiple modules unless imported explicitly.

Timeline diagram showing how different JavaScript loading modes affect HTML parsing and execution
Script ModeBlocks ParserOrder PreservedExecutes After DOMBlocks on CSSNotes
DefaultYesYesNoYes (if needed)Inline or external
AsyncNoNoNoYes (if needed)Fastest, unordered
DeferNoYesYesYes (if needed)Best for scripts that need DOM
ModuleNoNoYesYes (if needed)Supports imports

Summary:

  • Use defer for scripts that depend on the DOM and should execute in order.
  • Use async for independent scripts (e.g., analytics) that do not depend on DOM or other scripts.
  • Use type="module" for modern, modular JavaScript.

With the DOM and CSSOM ready, the browser combines them to create the Render Tree:

  • Render Tree: Contains only visible nodes and their computed styles.
  • Excludes: Non-visual nodes (like <head>, <script>, <meta>) and nodes with display: none.
  • Difference: display: none removes nodes from the render tree; visibility: hidden keeps them in the tree but makes them invisible (they still occupy space).

Render Tree

Render tree showing the combination of DOM and CSSOM trees with only visible elements included

The browser walks the Render Tree to calculate the exact size and position of each node:

  • Box Model: Determines width, height, and coordinates for every element.
  • Triggers: Any change affecting geometry (e.g., resizing, changing font size, adding/removing elements) can trigger a reflow.
  • Performance: Layout is expensive, especially if triggered repeatedly (see Layout Thrashing below).

With geometry calculated, the browser fills in the pixels for each node:

  • Painting: Drawing text, colors, images, borders, etc., onto layers in memory.
  • Optimization: Modern browsers only repaint invalidated regions, not the entire screen.
  • Output: Bitmaps/textures representing different parts of the page.

Modern browsers paint certain elements onto separate layers, which are then composited together:

  • Compositor Thread: Separate from the main thread, handles assembling layers into the final image.
  • Triggers for Layers: CSS properties like transform, opacity, will-change, 3D transforms, <video>, <canvas>, position: fixed/sticky, and CSS filters.
  • Performance: Animations using only transform and opacity can be handled entirely by the compositor, skipping layout and paint for smooth 60fps animations.

Modern browsers employ a preload scanner—a speculative, parallel HTML parser that discovers and fetches resources (images, scripts, styles) even while the main parser is blocked. This optimization is only effective if resources are declared in the initial HTML. Anti-patterns that defeat the preload scanner include:

  • Loading critical images via CSS background-image (use <img> with src instead).
  • Dynamically injecting scripts with JavaScript.
  • Fully client-side rendered markup (SPAs without SSR/SSG).
  • Incorrect lazy-loading of above-the-fold images.
  • Excessive inlining of large resources.

Best Practice: Declare all critical resources in the initial HTML. Use SSR/SSG for critical content, and <img> for important images.


CSS Render Blocking:

CSS is render-blocking because the browser needs to know the final computed styles before it can paint any pixels. If CSS were not render-blocking, users might see:

  1. Flash of Unstyled Content (FOUC): Content appears briefly without styles applied
  2. Layout Shifts: Elements changing position as styles load
  3. Incorrect Layout: Elements positioned incorrectly due to missing style information

JavaScript Parser Blocking:

JavaScript can block HTML parsing because:

  1. DOM Access: Scripts may need to access DOM elements that haven’t been parsed yet
  2. Style Access: Scripts may need to read computed styles that depend on CSS
  3. Order Dependencies: Scripts may depend on the order of elements in the DOM

CSS Blocking Rendering:

<head>
<link rel="stylesheet" href="styles.css" />
<!-- Browser must wait for CSS to load and parse before rendering -->
</head>
<body>
<h1>This won't render until CSS is loaded</h1>
</body>

JavaScript Blocking Parsing:

<head>
<script src="app.js"></script>
<!-- HTML parser stops here until script loads and executes -->
</head>
<body>
<h1>This won't be parsed until script completes</h1>
</body>

JavaScript Blocking on CSS:

<head>
<link rel="stylesheet" href="styles.css" />
<script>
// This script must wait for CSS to load because it accesses styles
const element = document.querySelector(".styled-element")
const color = getComputedStyle(element).color
</script>
</head>

Layout thrashing occurs when JavaScript forces the browser to recalculate layout repeatedly:

// This causes layout thrashing
const elements = document.querySelectorAll(".item")
for (let i = 0; i < elements.length; i++) {
const width = elements[i].offsetWidth // Forces layout calculation
elements[i].style.width = width * 2 + "px" // Changes layout
// Next iteration will force another layout calculation
}

Why it’s expensive:

  • Each offsetWidth read forces the browser to calculate layout
  • Layout calculations involve traversing the entire render tree
  • Multiple layout calculations in a loop create exponential performance degradation

Invalidation Scope Issues:

// Forces recalculation of entire document
document.body.classList.add("dark-theme")

Why it’s expensive:

  • Changing styles on high-level elements affects the entire document tree
  • Browser must recalculate styles for all descendant elements
  • Can cause massive performance hits on large documents

Large CSS Selectors:

/* Expensive selector - requires more computation */
body div.container div.content div.article div.paragraph span.text {
color: red;
}

Why it’s expensive:

  • Complex selectors require more computation during style calculation
  • Browser must traverse more nodes to match the selector
  • Performance impact increases with DOM size and selector complexity

The protocol used to deliver resources fundamentally impacts CRP:

  • HTTP/1.1: Multiple TCP connections, limited parallelism, head-of-line blocking.
  • HTTP/2: Multiplexing over a single TCP connection, but still subject to TCP head-of-line blocking.
  • HTTP/3 (QUIC): Multiplexing over UDP, eliminates head-of-line blocking, faster handshakes, resilient to network changes.
FeatureHTTP/1.1HTTP/2HTTP/3 (QUIC)
ConnectionMultiple TCPSingle TCPSingle QUIC (UDP)
MultiplexingNoYesYes (Improved)
HOL BlockingYesYes (TCP-level)No (per-stream)
HandshakeSlowSlowFast (0-RTT)

The main thread handles:

  • HTML Parsing: Converting HTML into DOM
  • CSS Parsing: Converting CSS into CSSOM
  • JavaScript Execution: Running JavaScript code
  • Style Calculation: Computing final styles
  • Layout: Calculating element positions and sizes
  • Paint: Drawing pixels to the screen

The compositor thread handles:

  • Layer Assembly: Combining painted layers into final image
  • Scrolling: Smooth scrolling animations
  • Transform/Opacity Animations: GPU-accelerated animations

Main Thread Blocking:

  • Long JavaScript tasks block all rendering
  • Heavy style calculations prevent layout and paint
  • Layout thrashing forces repeated main thread work

Compositor Thread Benefits:

  • Transform/opacity animations run on separate thread
  • Scrolling remains smooth even with main thread work
  • GPU acceleration for visual effects

  • Main thread: Shows DOM construction, style calculation, layout, paint, and compositing.
  • Long purple blocks: Indicate heavy style/layout work (often due to layout thrashing).
  • Green blocks: Paint and compositing.
  • Waterfall: Visualizes resource dependencies and blocking.
  • Eliminate render-blocking resources: Lists CSS/JS files delaying First Contentful Paint.
  • Critical request chain: Shows dependency graph for initial render.
  • Visualize compositor layers: Diagnose layer explosions and compositing issues.

Best Practice: Always test under simulated mobile network and CPU conditions.


Understanding the Critical Rendering Path is fundamental to web development. The key insights are:

  • CSS blocks rendering because browsers need complete style information before painting
  • JavaScript blocks parsing when it needs to access DOM or styles
  • Layout thrashing occurs when JavaScript forces repeated layout calculations
  • The main thread handles parsing, styling, layout, and painting sequentially
  • The compositor thread handles GPU-accelerated animations and scrolling
  • Network protocols affect how resources are delivered and can create bottlenecks
  • The preload scanner helps parallelize resource discovery but only works with declarative resources

The CRP is not a simple linear process—it involves multiple threads, speculative parsing, and complex dependencies between resources. Understanding these relationships helps developers write more efficient code and avoid common performance pitfalls.


  • Downloaded from Alex Xu Twitter post.

CRP from Bytebytego

Comprehensive critical rendering path diagram from Bytebytego showing the complete browser rendering pipeline

Tags

Read more

  • Previous

    HTTP Evolution: HTTP/1.1 to HTTP/3

    25 min read

    Master HTTP protocol evolution from HTTP/1.1 through HTTP/2 to HTTP/3, understanding TLS handshakes, head-of-line blocking, and browser protocol selection algorithms.

  • Next in series: Frontend Architecture & Patterns

    Microfrontends Architecture

    15 min read

    Learn how to scale frontend development with microfrontends, enabling team autonomy, independent deployments, and domain-driven boundaries for large-scale applications.