18 min read
Part of Series: Web Performance Optimization

Critical Rendering Path: A Modern, Comprehensive Guide

Understanding the Critical Rendering Path (CRP) is essential for web performance optimization. This guide synthesizes foundational concepts with modern browser architecture, advanced bottlenecks, and actionable optimization strategies.

The Critical Rendering Path (CRP) is the sequence of steps browsers execute to convert HTML, CSS, and JavaScript into the pixels users see. A deep understanding of this path is non-negotiable for web performance. Modern browsers are not simple, linear engines—they are multi-threaded, speculative, and highly optimized. Optimizing CRP means understanding both the theory and the practical bottlenecks that affect real-world sites.

MetricWhat CRP Stage Influences It MostTypical BottleneckOptimization Lever
First Contentful Paint (FCP)HTML → DOM, CSS → CSSOMRender-blocking CSSInline critical CSS, media/print, preload
Largest Contentful Paint (LCP)Layout → PaintHeavy hero images, slow resource fetchOptimized images, priority hints, server push
Interaction to Next Paint (INP)Style-Calc, Layout, Paint, CompositeLong tasks, forced reflowsBreak tasks, eliminate layout thrash
Frame Budget (≈16 ms)Style → Layout → Paint → CompositeExpensive paints, too many layersGPU-friendly properties (transform, opacity), layer budgets

The modern CRP is best understood as a six-stage pipeline. Each stage is critical for understanding performance bottlenecks and optimization opportunities.

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

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;
}

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.

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

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.


Now that we understand the Critical Rendering Path, let’s explore comprehensive optimization techniques organized by resource type and impact area.

Inline the CSS required for above-the-fold content directly in the <head> to eliminate render-blocking requests:

<head>
<style>
/* Critical above-the-fold styles */
.hero {
width: 100%;
height: 400px;
}
.header {
position: fixed;
top: 0;
}
</style>
<link rel="stylesheet" href="non-critical.css" media="print" onload="this.media='all'" />
</head>
  • Minify CSS to reduce file size by removing whitespace and comments.
  • Enable gzip/Brotli compression on your server.
  • Use tools like cssnano or clean-css for automated minification.

Split CSS into critical and non-critical chunks:

/* critical.css - Above-the-fold styles */
.hero,
.header,
.nav {
/* critical styles */
}
/* non-critical.css - Below-the-fold styles */
.footer,
.sidebar {
/* non-critical styles */
}

Use CSS containment to isolate layout, style, and paint operations:

.widget {
contain: layout style paint; /* Isolates rendering */
}
.isolated-component {
contain: strict; /* All containment types */
}

Skip rendering work for off-screen content:

.long-article-section {
content-visibility: auto;
contain-intrinsic-size: 1500px; /* Placeholder height */
}

Optimize animations by hinting which properties will change:

.animated-element {
will-change: transform, opacity; /* Hint to browser */
}

Note: Use will-change sparingly as it can create compositor layers and consume memory.

Split large JavaScript bundles into smaller chunks loaded on demand:

// Dynamic import for code splitting
const loadAnalytics = () => import("./analytics.js")
// Lazy load when needed
if (userInteracts) {
loadAnalytics().then((module) => module.init())
}

Remove unused code during bundling (works best with ES modules):

// Only used functions are included in the bundle
import { usedFunction } from "./utils.js"
// unusedFunction is eliminated from the final bundle
  • Use modern bundlers (Webpack, Vite, Rollup) with tree shaking.
  • Implement vendor chunking to separate third-party libraries.
  • Use dynamic imports for route-based code splitting.

Batch DOM reads and writes to avoid forced synchronous reflows:

// Anti-pattern: Causes layout thrashing
const elements = document.querySelectorAll(".box")
for (let i = 0; i < elements.length; i++) {
const newWidth = elements[i].offsetWidth // READ
elements[i].style.width = newWidth / 2 + "px" // WRITE
}
// Solution: Batch reads, then writes
const elements = document.querySelectorAll(".box")
const widths = []
for (let i = 0; i < elements.length; i++) {
widths.push(elements[i].offsetWidth)
}
for (let i = 0; i < elements.length; i++) {
elements[i].style.width = widths[i] / 2 + "px"
}

Implement efficient lazy loading and infinite scrolling:

const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src
observer.unobserve(entry.target)
}
})
})
document.querySelectorAll("img[data-src]").forEach((img) => {
observer.observe(img)
})

Implement advanced caching strategies:

// Cache-first strategy for static assets
self.addEventListener("fetch", (event) => {
if (event.request.destination === "image") {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request)
}),
)
}
})

Use WebP, AVIF, or JPEG XL for better compression:

<picture>
<source srcset="image.avif" type="image/avif" />
<source srcset="image.webp" type="image/webp" />
<img src="image.jpg" alt="Description" />
</picture>

Provide appropriate image sizes for different viewports:

<img
src="hero.jpg"
srcset="hero-300.jpg 300w, hero-600.jpg 600w, hero-900.jpg 900w"
sizes="(max-width: 600px) 300px, (max-width: 900px) 600px, 900px"
alt="Hero image"
/>

Use native lazy loading for below-the-fold images:

<img src="image.jpg" loading="lazy" alt="Lazy loaded image" />
  • Use tools like ImageOptim, TinyPNG, or Squoosh for compression.
  • Implement progressive JPEG loading for better perceived performance.
  • Consider using WebP with fallbacks for broader browser support.

Preload critical above-the-fold images:

<link rel="preload" as="image" href="hero-image.jpg" />

Control how fonts are displayed during loading:

@font-face {
font-family: "MyFont";
src: url("font.woff2") format("woff2");
font-display: swap; /* Show fallback immediately, swap when loaded */
}

Preload critical fonts to avoid layout shifts:

<link rel="preload" href="/fonts/critical-font.woff2" as="font" type="font/woff2" crossorigin />

Include only the characters you need to reduce file size:

@font-face {
font-family: "MyFont";
src: url("font-subset.woff2") format("woff2");
unicode-range:
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

Control font loading programmatically:

if ("fonts" in document) {
document.fonts.load("1em MyFont").then(() => {
// Font is loaded and ready
})
}

Use resource hints to optimize loading:

<!-- Establish early connections -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="dns-prefetch" href="https://www.google-analytics.com" />
<!-- Preload critical resources -->
<link rel="preload" href="/fonts/critical.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/css/critical.css" as="style" />
<link rel="preload" href="/js/critical.js" as="script" />
<!-- Prefetch likely future resources -->
<link rel="prefetch" href="/dashboard.css" as="style" />

Control resource loading priority:

<link rel="preload" href="critical.js" as="script" fetchpriority="high" />
<img src="hero.jpg" fetchpriority="high" alt="Hero" />
<img src="below-fold.jpg" fetchpriority="low" alt="Below fold" />

Compression:

  • Enable gzip/Brotli compression on your server.
  • Use appropriate compression levels for different content types.

Caching Strategies:

  • Set appropriate Cache-Control headers for different resource types.
  • Use versioning or content hashing for cache busting.
  • Implement service workers for advanced caching strategies.

SSR Benefits:

  • Faster First Contentful Paint (FCP)
  • Better SEO
  • Improved Core Web Vitals
  • Critical content available immediately

SSG Benefits:

  • Pre-rendered pages at build time
  • Excellent performance
  • Reduced server load
  • Perfect for content-heavy sites

Implementation Examples:

// Next.js SSR example
export async function getServerSideProps() {
const data = await fetchData()
return { props: { data } }
}
// Astro SSG example
export async function getStaticProps() {
const posts = await getPosts()
return { props: { posts } }
}

Edge Caching:

  • Distribute content globally
  • Reduce latency for users worldwide
  • Implement cache warming strategies

CDN Configuration:

# Nginx CDN configuration
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}

Push critical resources before the browser requests them:

// Express.js with HTTP/2 push
app.get("/", (req, res) => {
res.push("/css/critical.css", {
req: { accept: "text/css" },
res: { "content-type": "text/css" },
})
res.send(html)
})
  • Use tools like Webpack Bundle Analyzer to identify large dependencies.
  • Implement code splitting based on routes and features.
  • Remove unused dependencies and polyfills.
  • Consider using modern JavaScript features with appropriate fallbacks.
  • Implement Real User Monitoring (RUM) to track Core Web Vitals.
  • Use Performance API to measure custom metrics.
  • Set up alerts for performance regressions.
  • Monitor and optimize based on real-world data.

CSS Paint API / PaintWorklet

Move custom painting operations off the main thread to prevent blocking the CRP:

// Register a custom paint worklet
CSS.paintWorklet.addModule('custom-paint.js');
// Use in CSS
.custom-element {
background: paint(custom-pattern);
}
custom-paint.js
class CustomPatternPainter {
paint(ctx, size, properties) {
// Custom painting logic runs off main thread
ctx.fillStyle = "#f0f0f0"
ctx.fillRect(0, 0, size.width, size.height)
// Complex patterns without blocking main thread
for (let i = 0; i < size.width; i += 10) {
ctx.strokeStyle = "#333"
ctx.beginPath()
ctx.moveTo(i, 0)
ctx.lineTo(i, size.height)
ctx.stroke()
}
}
}
registerPaint("custom-pattern", CustomPatternPainter)

Benefits:

  • Custom painting operations don’t block the main thread
  • Enables complex visual effects without impacting CRP
  • Better performance for animated custom backgrounds

AnimationWorklet

Create high-frequency animations at 120 FPS without main thread involvement:

// Register animation worklet
CSS.animationWorklet.addModule('scroll-animation.js');
// Use in CSS
.scroll-animated {
animation: scroll-animation 1s linear;
}
scroll-animation.js
class ScrollDrivenAnimation {
constructor(options) {
this.options = options
}
animate(currentTime, effect) {
// Animation logic runs at 120 FPS on separate thread
const progress = currentTime / 1000 // Convert to seconds
const transform = `translateY(${progress * 100}px)`
effect.localTime = currentTime
effect.target.style.transform = transform
}
}
registerAnimator("scroll-animation", ScrollDrivenAnimation)

Benefits:

  • Animations run at 120 FPS on dedicated thread
  • No main thread blocking during animations
  • Smooth scrolling and complex animations
  • Better battery life on mobile devices

Model-Viewer for 3D Content

Leverage GPU for 3D rendering without blocking the CRP:

<!-- Load 3D model without blocking main thread -->
<model-viewer
src="model.glb"
alt="3D Model"
camera-controls
auto-rotate
shadow-intensity="1"
environment-image="neutral"
exposure="1"
shadow-softness="0.5"
>
</model-viewer>
<!-- With custom loading and error handling -->
<model-viewer
src="model.glb"
alt="3D Model"
loading="eager"
reveal="auto"
ar
ar-modes="webxr scene-viewer quick-look"
camera-controls
auto-rotate
>
<!-- Loading placeholder -->
<div slot="progress-bar" class="progress-bar">
<div class="progress-bar-fill"></div>
</div>
<!-- Error fallback -->
<div slot="error" class="error-message">Unable to load 3D model</div>
</model-viewer>

Benefits:

  • 3D rendering happens on GPU, not CPU
  • No main thread blocking for complex 3D scenes
  • Hardware acceleration for smooth performance
  • Progressive loading with placeholders
  • AR support for mobile devices

Implementation Considerations:

  • Use loading="lazy" for below-the-fold 3D content
  • Implement proper fallbacks for unsupported browsers
  • Consider using IntersectionObserver to load models only when needed
  • Optimize 3D models (reduce polygon count, compress textures)

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)

Understanding what NOT to do is as important as knowing the right techniques. These anti-patterns can severely impact your CRP performance.

Invalidation Scope Issues

Changing a class on <body> forces full-tree recalculation:

// ❌ BAD: Forces recalculation of entire document
document.body.classList.add("dark-theme")
// ✅ GOOD: Target specific elements
document.querySelector(".theme-container").classList.add("dark-theme")

Why it’s bad: When you modify styles on high-level elements like <body> or <html>, the browser must recalculate styles for the entire document tree, causing massive performance hits.

Large CSS Selectors

User Selector performance tracing to measure the impact. Enable experimental “Selector Stats” in Edge/Chrome devtools

/* ❌ BAD: Expensive selector */
body div.container div.content div.article div.paragraph span.text {
color: red;
}
/* ✅ GOOD: Specific, efficient selector */
.article-text {
color: red;
}

Why it’s bad: Complex selectors require more computation during style calculation, especially when the DOM changes.

Read-Write Cycles in Loops

// ❌ BAD: Forces reflow on every iteration
const elements = document.querySelectorAll(".item")
for (let i = 0; i < elements.length; i++) {
const width = elements[i].offsetWidth // READ
elements[i].style.width = width * 2 + "px" // WRITE
}
// ✅ GOOD: Batch reads and writes
const elements = document.querySelectorAll(".item")
const widths = []
for (let i = 0; i < elements.length; i++) {
widths.push(elements[i].offsetWidth) // All READS
}
for (let i = 0; i < elements.length; i++) {
elements[i].style.width = widths[i] * 2 + "px" // All WRITES
}

Why it’s bad: Each read forces a synchronous reflow, making the loop exponentially slower.

Blocking Critical Resources

<!-- ❌ BAD: Blocks rendering -->
<head>
<link rel="stylesheet" href="non-critical.css" />
<script src="analytics.js"></script>
</head>
<!-- ✅ GOOD: Non-blocking loading -->
<head>
<link rel="stylesheet" href="non-critical.css" media="print" onload="this.media='all'" />
<script src="analytics.js" async></script>
</head>

Why it’s bad: Render-blocking resources delay First Contentful Paint and Largest Contentful Paint.

Hidden Resources from Preload Scanner

/* ❌ BAD: Image hidden from preload scanner */
.hero {
background-image: url("hero-image.jpg");
}
<!-- ✅ GOOD: Discoverable by preload scanner -->
<img src="hero-image.jpg" alt="Hero" class="hero" />

Why it’s bad: The preload scanner can’t discover resources in CSS, delaying their loading.

Excessive DOM Queries

// ❌ BAD: Multiple DOM queries
for (let i = 0; i < 1000; i++) {
const element = document.querySelector(".item") // Expensive query
element.style.color = "red"
}
// ✅ GOOD: Single query, cache reference
const element = document.querySelector(".item")
for (let i = 0; i < 1000; i++) {
element.style.color = "red"
}

Why it’s bad: DOM queries are expensive operations that should be minimized and cached.

Creating Elements in Loops

// ❌ BAD: Creates elements one by one
for (let i = 0; i < 1000; i++) {
const div = document.createElement("div")
div.textContent = `Item ${i}`
document.body.appendChild(div) // Forces reflow each time
}
// ✅ GOOD: Use DocumentFragment
const fragment = document.createDocumentFragment()
for (let i = 0; i < 1000; i++) {
const div = document.createElement("div")
div.textContent = `Item ${i}`
fragment.appendChild(div)
}
document.body.appendChild(fragment) // Single reflow

Why it’s bad: Each appendChild forces a reflow. DocumentFragment batches all changes.

Animating Layout-Triggering Properties

/* ❌ BAD: Triggers layout on every frame */
.animated {
animation: bad-animation 1s infinite;
}
@keyframes bad-animation {
0% {
width: 100px;
}
50% {
width: 200px;
}
100% {
width: 100px;
}
}
/* ✅ GOOD: Only animates compositor-friendly properties */
.animated {
animation: good-animation 1s infinite;
}
@keyframes good-animation {
0% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}

Why it’s bad: Animating width, height, top, left, etc. triggers layout on every frame, causing jank.

Overusing will-change

/* ❌ BAD: Creates unnecessary layers */
.everything {
will-change: transform, opacity, background-color;
}
/* ✅ GOOD: Only hint what will actually change */
.animated-element {
will-change: transform;
}

Why it’s bad: will-change creates compositor layers that consume memory. Use sparingly.

Event Listener Leaks

// ❌ BAD: Creates new listener on every render
function BadComponent() {
document.addEventListener("scroll", () => {
// Handle scroll
})
}
// ✅ GOOD: Clean up listeners
function GoodComponent() {
const handleScroll = () => {
// Handle scroll
}
document.addEventListener("scroll", handleScroll)
return () => {
document.removeEventListener("scroll", handleScroll)
}
}

Why it’s bad: Unremoved event listeners cause memory leaks and performance degradation.

Large Bundle Sizes

// ❌ BAD: Imports entire library
import _ from "lodash"
// ✅ GOOD: Import only what you need
import debounce from "lodash/debounce"

Why it’s bad: Large bundles increase download time and parsing time, blocking the CRP.


  • 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.

Use this checklist to audit and optimize your Critical Rendering Path:

  • Critical CSS extracted & inlined?

    • Above-the-fold styles inlined in <head>
    • Non-critical CSS loaded non-blocking
  • All render-blocking JS deferred?

    • Scripts use async, defer, or type="module"
    • No blocking scripts in <head>
  • Largest image preloaded with correct dimensions?

    • Hero/LCP image preloaded with rel="preload"
    • Responsive images with proper srcset and sizes
  • DOM ≤ 1,500 nodes above the fold?

    • Minimal DOM complexity for initial render
    • Complex content deferred below the fold
  • Long tasks broken below 50 ms?

    • JavaScript tasks split into smaller chunks
    • Use requestIdleCallback for non-critical work
  • No forced reflows in hot loops?

    • DOM reads and writes batched separately
    • Layout thrashing eliminated
  • Layer count under GPU budget?

    • Reasonable number of compositor layers
    • will-change used sparingly
  • Continuous animations use only transform/opacity?

    • No layout-triggering properties in animations
    • GPU-accelerated animations only
  • contain and content-visibility applied where safe?

    • CSS containment for isolated components
    • content-visibility: auto for off-screen content
  • Field metrics (FCP, LCP, INP) green in Web Vitals dashboard?

    • First Contentful Paint < 1.8s
    • Largest Contentful Paint < 2.5s
    • Interaction to Next Paint < 200ms

Additional Checks:

  • Resource hints (preconnect, preload) implemented
  • Images optimized (WebP/AVIF, compression, lazy loading)
  • Fonts optimized (font-display: swap, preloading)
  • HTTP/2 or HTTP/3 enabled
  • CDN configured properly
  • Service worker caching strategy implemented
  • Bundle size optimized (tree shaking, code splitting)
  • Adopt a modern, parallel mental model: The CRP is not linear—embrace preload scanner and compositor thread parallelism.
  • Prioritize declaratively: Declare critical resources in HTML, use SSR/SSG, and avoid hiding resources in CSS/JS.
  • Master resource prioritization: Use preconnect, preload, defer, and non-blocking CSS techniques judiciously.
  • Optimize beyond initial load: Batch DOM reads/writes, use content-visibility, and stick to compositor-friendly animations.
  • Implement advanced techniques: Use modern image formats, font optimization, CSS containment, and service workers.
  • Leverage modern protocols: Upgrade to HTTP/2 or HTTP/3 when possible, implement server push for critical resources.
  • Measure, don’t guess: Use DevTools, Lighthouse, and always test under real-world conditions.
  • Focus on Core Web Vitals: Optimize for LCP, FID, and CLS to improve user experience and search rankings.

  • Downloaded from Alex Xu Twitter post.

CRP from Bytebytego

Tags

Read more