Suggest an editImprove this articleRefine the answer for “What is critical rendering path (crp) in browser”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Critical rendering path (CRP)** is the sequence of steps a browser takes to convert HTML, CSS, and JavaScript into visible pixels on screen. Steps: DOM construction → CSSOM → Render Tree → Layout → Paint → Composite **Key point:** CSS and synchronous JS block every step after them. Use `defer`/`async` on scripts and inline critical CSS to keep FCP under 1.8s.Shown above the full answer for quick recall.Answer (EN)Image**Critical rendering path (CRP)** is the sequence of steps a browser takes to convert HTML, CSS, and JavaScript into visible pixels on screen, starting from the first byte of HTML and ending at the first painted frame. ## Theory ### TL;DR - CRP works like a factory assembly line: HTML builds the frame (DOM), CSS adds styling (CSSOM), but a blocking `<script>` halts the whole line until it finishes - Main goal: minimize time to First Contentful Paint (FCP); Chrome targets under 1.8s - Steps in order: DOM construction → CSSOM → Render Tree → Layout → Paint → Composite - Blocking resources (CSS files, synchronous JS) delay every step that follows them - Decision rule: FCP over 1.8s on mobile? Start with `async`/`defer` on scripts, then preload critical CSS ### Quick example ```html <!DOCTYPE html> <html> <head> <!-- Blocks parsing until downloaded and parsed --> <link rel="stylesheet" href="styles.css"> <!-- Blocks parsing until downloaded and executed --> <script src="script.js"></script> </head> <body> <!-- Won't paint until CRP completes --> <h1>Visible content</h1> </body> </html> ``` The browser stops at `<script>`, downloads and runs `script.js`, then builds DOM and CSSOM, merges them into a render tree, calculates layout, and finally paints. Each blocking resource adds 200-500ms to FCP. You can see this directly in Chrome DevTools Network tab. ### How the browser builds the CRP Six steps, always in this order. **DOM construction.** The browser parses HTML byte-by-byte into a tree of nodes. Chrome's Blink engine uses a pre-parser that speculatively fetches CSS and JS references while the main parser processes tokens. The `<html>`, `<head>`, `<body>` tags you write become `Document`, `Element`, and `Text` nodes. **CSSOM construction.** CSS is render-blocking by design. The browser cannot display anything without knowing which styles apply to which nodes. Every `<link rel="stylesheet">` triggers a fetch, and until that file downloads and parses, painting stops. `@import` inside a stylesheet makes this worse because it triggers a serial fetch, meaning files load one after another instead of in parallel. **Render Tree.** DOM and CSSOM merge into a render tree that contains only visible nodes with computed styles. Elements with `display: none` are excluded entirely. Elements with `visibility: hidden` stay in but render as blank space. **Layout (Reflow).** The browser calculates the exact size and position of every render tree node. This step is expensive when triggered repeatedly. Any JS that reads layout properties like `offsetWidth` after mutating the DOM forces a synchronous reflow. **Paint.** The browser converts layout information into actual pixels, per layer. Text, borders, shadows, colors, all rasterized here. **Composite.** Individual layers (especially those using CSS `transform` or `opacity`) get assembled by the compositor thread and sent to the GPU. This is why `transform: translateZ(0)` can push an element to its own layer and skip the paint step on animation. ### Key difference: CRP vs full page load Full page load includes every resource: below-fold images, lazy-loaded scripts, third-party widgets. CRP focuses only on what is needed to paint the first visible frame. Chrome can paint above-the-fold content in roughly 100ms if nothing blocks it. Waiting for the full page can mean 2+ seconds. That gap is exactly what CRP optimization targets. ### When to optimize CRP - FCP above 1.8s on mobile: preload critical CSS with `<link rel="preload">`, inline it if under 10KB - JS-heavy SPA: add `async` or `defer` to script tags to unblock parsing - Hero image above the fold: use `preload` with `fetchpriority="high"` - CSS with `@import`: replace with concatenated files via webpack `MiniCssExtractPlugin` - Legacy site: minify HTML and CSS, remove render-blocking resources ### Common mistakes **1. Synchronous `<script>` in `<head>`** ```html <!-- Blocks DOM parsing for the full download + execution time --> <script src="app.js"></script> ``` The browser stops parsing HTML the moment it hits this tag. A 1MB bundle on slow 3G can add a full second to FCP. Use `defer` for scripts that need DOM access, `async` for independent scripts like analytics. **2. CSS `@import` in a critical stylesheet** ```css /* reset.css fetched serially, doubles CSS load time */ @import url('reset.css'); ``` The browser cannot discover `reset.css` until it finishes downloading and parsing the parent file. Lighthouse flags this. Bundle imports at build time instead. **3. Forced synchronous reflow in a loop** ```javascript // Bad: from real dashboard code for (let i = 0; i < 1000; i++) { divs[i].style.height = '20px'; // Mutation console.log(divs[i].getBoundingClientRect().height); // Forces reflow per iteration } // Result: 60fps drops to 5fps, FCP increases by ~2s ``` In production, this pattern quietly kills performance more often than any other CRP issue, especially in React portals and dashboard components that measure overlay dimensions. Fix: batch all mutations first, read layout properties after the loop. **4. Large inline `<style>` block above the fold** ```html <style>/* 50KB of styles */</style> ``` The parser waits for the full parse before moving on. Extract only critical above-fold CSS (under 14KB) and async-load the rest. **5. `document.write()` after initial load** ```javascript document.write('<div>Dynamic content</div>'); // After DOMContentLoaded ``` This triggers a full CRP recalculation and briefly blanks the screen. Use `element.appendChild()` instead. ### Real-world usage - **Next.js**: automatically generates `<link rel="preload">` for `_app.js` and critical chunks, targeting sub-1s FCP - **Gatsby**: `gatsby-plugin-critical` splits CSS and inlines above-fold styles, used across 100k+ sites with roughly 50% FCP improvement - **Create React App**: the default template uses `defer` on the main bundle but ships a blocking `main.css`; fix with the `critters` plugin for critical CSS extraction - **Webpack**: `HtmlWebpackPlugin` + `Critters` plugin inline critical CSS automatically in SPA builds - **React portals**: measuring overlay dimensions with `getBoundingClientRect()` inside render loops is a known CRP performance issue in large dashboards ### Follow-up questions **Q:** What is the difference between CRP and LCP? **A:** CRP is the full process from first byte to first painted frame. LCP (Largest Contentful Paint) is a Core Web Vital that measures how long the largest visible image or text block takes to appear. LCP above 2.5s is flagged as poor. CRP optimization usually improves LCP as a side effect. **Q:** How do `async` and `defer` differ in their effect on CRP? **A:** `async` downloads the script in parallel but executes it the moment download finishes, which can interrupt HTML parsing. `defer` also downloads in parallel but delays execution until after the DOM is fully parsed. For most app scripts, `defer` is the right choice. `async` works for scripts with no DOM dependencies, like analytics. **Q:** How do you measure CRP steps in Chrome DevTools? **A:** Open the Performance tab, record a page load, then filter for "Recalculate Style", "Layout", and "Paint" events. Each maps directly to a CRP step. Aim for under 100ms per frame. Yellow spikes in the flame chart usually point to forced reflows from JavaScript. **Q:** How does a Service Worker affect CRP in a PWA? **A:** The Service Worker intercepts fetch requests and can serve cached responses instantly, which speeds up CRP significantly on repeat visits. The `stale-while-revalidate` strategy returns cached HTML immediately while fetching an update in the background. The trade-off is that the first paint may show slightly stale content. **Q (senior level):** You have a news site with 50+ hero images and user-specific CSS. How do you optimize CRP? **A:** Preload the top 3 images with `fetchpriority="high"` so they compete for bandwidth first. Generate critical CSS per user segment server-side using PurgeCSS to strip unused rules. Lazy-load everything below the fold with `loading="lazy"`. Measure with the `web-vitals` library via RUM data, not just Lighthouse. This approach reduced FCP by around 40% at high-traffic news sites. ## Examples ### Blocking vs non-blocking scripts ```html <!-- Version 1: blocks rendering --> <head> <script src="analytics.js"></script> </head> <!-- Version 2: async for scripts with no DOM dependency --> <head> <script src="analytics.js" async></script> </head> <!-- Version 3: defer for scripts that need DOM --> <head> <script src="app.js" defer></script> </head> ``` `async` lets the browser keep parsing HTML while the script downloads, but execution happens the moment the download finishes, which can still interrupt parsing mid-way. `defer` guarantees execution after the full DOM is ready. For analytics and third-party scripts with no DOM dependencies, `async` is fine. For your main app bundle, use `defer`. ### Critical CSS extraction in a React app ```html <!-- public/index.html - unoptimized Create React App template --> <head> <!-- Render-blocking: delays FCP by ~300ms --> <link rel="stylesheet" href="/static/css/main.abc123.css"> </head> <body> <div id="root"></div> <!-- Good: defer keeps HTML parsing unblocked --> <script src="/static/js/main.abc123.js" defer></script> </body> ``` Without critical CSS extraction, Lighthouse reports FCP around 2.5s for a typical CRA app. Adding the `critters` plugin to your webpack config inlines above-fold styles directly in `<head>` and async-loads the rest. FCP drops to under 1s on most connections. ### The forced reflow loop trap Any time you read a layout property immediately after mutating the DOM, the browser has to stop and recalculate layout synchronously before it can return the value. ```javascript // Bad: forces a reflow on every iteration const divs = document.querySelectorAll('.item'); for (let i = 0; i < divs.length; i++) { divs[i].style.height = '20px'; // Mutation const height = divs[i].getBoundingClientRect().height; // Forces reflow console.log(height); } // Good: batch mutations, then batch reads for (let i = 0; i < divs.length; i++) { divs[i].style.height = '20px'; // All mutations first } for (let i = 0; i < divs.length; i++) { const height = divs[i].getBoundingClientRect().height; // All reads after console.log(height); } ``` In a Chrome Performance trace, the bad version shows yellow "Layout" spikes after every style mutation and frame rate drops from 60fps to 5fps. The good version triggers layout once. Both versions produce the same output, so the bug is invisible without profiling.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.