Overview
The critical rendering path is the sequence of steps the browser takes to convert HTML, CSS, and JavaScript into a rendered page. Optimizing this path is crucial for fast initial page loads and excellent user experience.
The critical rendering path directly impacts First Contentful Paint (FCP) and Largest Contentful Paint (LCP), two key Core Web Vitals metrics.
Understanding the rendering path
The browser follows these steps to render a page:
Parse HTML
The browser parses HTML to construct the DOM (Document Object Model) tree.
Parse CSS
CSS files are parsed to construct the CSSOM (CSS Object Model) tree.
Execute JavaScript
JavaScript can modify both DOM and CSSOM, potentially blocking rendering.
Construct render tree
DOM and CSSOM are combined to create the render tree (only visible elements).
Layout
Calculate exact position and size of each element.
Paint
Draw pixels to the screen.
CSS is render-blocking by default. JavaScript is both parser-blocking and render-blocking. These are the primary optimization targets.
Eliminate render-blocking resources
Render-blocking resources prevent the browser from displaying content until they’re downloaded and processed.
Identifying render-blocking resources
Lighthouse
Chrome DevTools
WebPageTest
Run a Lighthouse audit and check the “Eliminate render-blocking resources” opportunity. lighthouse https://example.com --view
Look for:
Stylesheets in <head>
Synchronous <script> tags
Large CSS files
Unused CSS rules
Open DevTools (F12)
Go to Performance tab
Record page load
Look for long yellow/purple blocks labeled “Parse Stylesheet” or “Evaluate Script”
These blocks show render-blocking resources delaying First Contentful Paint. Visit WebPageTest and analyze your site. Check the waterfall chart:
Resources with orange/red start bars are render-blocking
The “Start Render” line shows when blocking ends
Async and defer scripts
Blocking (default)
Async
Defer
Module scripts (defer by default)
<!-- Blocks HTML parsing until downloaded and executed -->
< script src = "script.js" ></ script >
< div > This content waits for script.js </ div >
Use async for scripts that:
Don’t depend on other scripts
Don’t modify the DOM before it’s ready
Can execute in any order
Examples: Analytics, ads, social media widgets< script src = "https://www.googletagmanager.com/gtag/js" async ></ script >
< script src = "//connect.facebook.net/en_US/sdk.js" async ></ script >
Use defer for scripts that:
Need the full DOM to be parsed
Depend on other scripts (execution order matters)
Can wait until HTML parsing completes
Examples: Main application code, UI libraries, DOM manipulation< script src = "jquery.js" defer ></ script >
< script src = "app.js" defer ></ script >
<!-- app.js executes after jquery.js -->
Use blocking scripts (no async/defer) only when:
Script must execute before page renders
Other scripts depend on it immediately
Examples: Critical polyfills, A/B testing that prevents FOUC<!-- Critical: must run before rendering -->
< script src = "critical-polyfill.js" ></ script >
Minimize critical CSS
Critical CSS is the minimum CSS needed to render above-the-fold content.
Identify critical styles
Determine which CSS rules are needed for above-the-fold content.
Inline critical CSS
Place critical CSS directly in the <head> to eliminate the network request.
Defer non-critical CSS
Load remaining CSS asynchronously to avoid blocking rendering.
Implementation approaches
Async CSS loading pattern
<!-- Preload CSS file -->
< link rel = "preload" href = "styles.css" as = "style" onload = " this . onload = null ; this . rel = 'stylesheet'" >
<!-- Fallback for browsers without JavaScript -->
< noscript >
< link rel = "stylesheet" href = "styles.css" >
</ noscript >
Use media queries to load CSS conditionally: < link rel = "stylesheet" href = "print.css" media = "print" >
< link rel = "stylesheet" href = "mobile.css" media = "(max-width: 768px)" >
Browsers still download all stylesheets but only block rendering for matching media queries.
Defer non-critical JavaScript
Defer JavaScript that’s not essential for initial rendering to improve page load performance.
Lazy loading JavaScript
Dynamic imports
Intersection Observer
Idle loading
// Load module only when needed
button . addEventListener ( 'click' , async () => {
const module = await import ( './heavy-feature.js' );
module . initialize ();
});
// Conditional loading
if ( window . innerWidth > 768 ) {
import ( './desktop-features.js' ). then ( module => {
module . init ();
});
}
Third-party script optimization
Self-host third-party scripts
Download and serve third-party scripts from your own domain to improve caching and reduce DNS lookups. <!-- Before: external CDN -->
< script src = "https://cdn.example.com/library.js" ></ script >
<!-- After: self-hosted -->
< script src = "/js/library.js" defer ></ script >
Benefits:
Control over caching headers
No third-party DNS lookup
Better privacy
Reduced connection overhead
Replace heavy third-party embeds with lightweight facades that load the real thing on interaction. import { useState } from 'react' ;
function YouTubeEmbed ({ videoId }) {
const [ loaded , setLoaded ] = useState ( false );
if ( ! loaded ) {
return (
< div
className = "youtube-facade"
style = { {
backgroundImage: `url(https://i.ytimg.com/vi/ ${ videoId } /hqdefault.jpg)` ,
cursor: 'pointer'
} }
onClick = { () => setLoaded ( true ) }
>
< div className = "play-button" > ▶ </ div >
</ div >
);
}
return (
< iframe
src = { `https://www.youtube.com/embed/ ${ videoId } ?autoplay=1` }
allow = "autoplay"
allowFullScreen
/>
);
}
Saves ~500KB by loading YouTube player only on click.
Delay non-essential scripts
Load analytics, social widgets, and ads after critical content. // Load after window load event
window . addEventListener ( 'load' , () => {
// Analytics
const analyticsScript = document . createElement ( 'script' );
analyticsScript . src = 'https://www.google-analytics.com/analytics.js' ;
document . head . appendChild ( analyticsScript );
// Social widgets
const fbScript = document . createElement ( 'script' );
fbScript . src = 'https://connect.facebook.net/en_US/sdk.js' ;
fbScript . async = true ;
document . body . appendChild ( fbScript );
});
Third-party scripts can significantly impact performance. Audit them regularly and remove unused ones.
Preload, prefetch, and preconnect strategies
Resource hints tell the browser to optimize loading for specific resources.
Resource hint types
Preload Load critical resources early in the page lifecycle. Priority: High
Use for: Critical fonts, CSS, scripts, images
Prefetch Load resources needed for future navigation during idle time. Priority: Low
Use for: Next page resources, likely user actions
Preconnect Establish early connections to important third-party origins. Priority: Medium
Use for: API domains, CDNs, fonts
DNS-prefetch Resolve DNS early for third-party domains. Priority: Low
Use for: Multiple third-party domains
Preload examples
Fonts
Images
Scripts and styles
<!-- Preload critical fonts to avoid FOIT/FOUT -->
< link
rel = "preload"
href = "/fonts/inter-var.woff2"
as = "font"
type = "font/woff2"
crossorigin
/>
< link
rel = "preload"
href = "/fonts/roboto-mono.woff2"
as = "font"
type = "font/woff2"
crossorigin
/>
< style >
@font-face {
font-family : 'Inter' ;
src : url ( '/fonts/inter-var.woff2' ) format ( 'woff2' );
font-display : swap ;
}
</ style >
Always include crossorigin attribute for font preloads, even for same-origin fonts.
<!-- Preload hero image to improve LCP -->
< link
rel = "preload"
href = "/images/hero.webp"
as = "image"
type = "image/webp"
/>
<!-- Responsive image preload -->
< link
rel = "preload"
href = "/images/hero-mobile.webp"
as = "image"
type = "image/webp"
media = "(max-width: 768px)"
/>
< link
rel = "preload"
href = "/images/hero-desktop.webp"
as = "image"
type = "image/webp"
media = "(min-width: 769px)"
/>
<!-- Preload critical CSS -->
< link rel = "preload" href = "/css/critical.css" as = "style" />
< link rel = "stylesheet" href = "/css/critical.css" />
<!-- Preload important scripts -->
< link rel = "preload" href = "/js/app.js" as = "script" />
< script src = "/js/app.js" defer ></ script >
<!-- Preload module scripts -->
< link rel = "modulepreload" href = "/js/module.js" />
< script type = "module" src = "/js/module.js" ></ script >
Prefetch examples
<!-- Prefetch next page -->
< link rel = "prefetch" href = "/page2.html" />
<!-- Prefetch route chunks -->
< link rel = "prefetch" href = "/js/dashboard-chunk.js" />
<!-- Prefetch API data -->
< link rel = "prefetch" href = "/api/user-data.json" />
<!-- Prefetch images for next page -->
< link rel = "prefetch" href = "/images/page2-hero.webp" as = "image" />
Prefetch resources during idle time: if ( 'requestIdleCallback' in window ) {
requestIdleCallback (() => {
const link = document . createElement ( 'link' );
link . rel = 'prefetch' ;
link . href = '/dashboard' ;
document . head . appendChild ( link );
});
}
Preconnect examples
Basic preconnect
DNS-prefetch fallback
Dynamic preconnect
<!-- Connect to API domain early -->
< link rel = "preconnect" href = "https://api.example.com" />
<!-- Connect to CDN -->
< link rel = "preconnect" href = "https://cdn.example.com" />
<!-- Connect to Google Fonts -->
< link rel = "preconnect" href = "https://fonts.googleapis.com" />
< link rel = "preconnect" href = "https://fonts.gstatic.com" crossorigin />
Resource hint decision tree
Is it critical for initial render?
Yes → Use preload No → Go to step 2
Will the user likely need it soon?
Yes → Use prefetch No → Go to step 3
Does it require a third-party connection?
Yes → Use preconnect or dns-prefetch No → Don’t use resource hints
Don’t overuse resource hints. Too many preloads can delay actual critical resources. Limit to 3-5 preloads per page.
Complete optimization example
<! DOCTYPE html >
< html lang = "en" >
< head >
< meta charset = "UTF-8" />
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" />
< title > Optimized Page </ title >
<!-- Preconnect to critical origins -->
< link rel = "preconnect" href = "https://fonts.googleapis.com" />
< link rel = "preconnect" href = "https://fonts.gstatic.com" crossorigin />
< link rel = "preconnect" href = "https://api.example.com" />
<!-- Preload critical fonts -->
< link
rel = "preload"
href = "/fonts/inter.woff2"
as = "font"
type = "font/woff2"
crossorigin
/>
<!-- Preload hero image (LCP element) -->
< link
rel = "preload"
href = "/images/hero.webp"
as = "image"
type = "image/webp"
fetchpriority = "high"
/>
<!-- Inline critical CSS -->
< style >
/* Critical above-the-fold styles */
body { margin : 0 ; font-family :Inter, system-ui , sans-serif }
.header { height : 60 px ; background : #fff ; box-shadow : 0 2 px 4 px rgba ( 0 , 0 , 0 , .1 )}
.hero { height : 400 px ; background : url ( /images/hero.webp ) center / cover }
</ style >
<!-- Async load full stylesheet -->
< link
rel = "preload"
href = "/css/styles.css"
as = "style"
onload = " this . onload = null ; this . rel = 'stylesheet'"
/>
< noscript >< link rel = "stylesheet" href = "/css/styles.css" /></ noscript >
<!-- Preload critical script -->
< link rel = "modulepreload" href = "/js/app.js" />
</ head >
< body >
< header class = "header" >
<!-- Header content -->
</ header >
< section class = "hero" >
<!-- Hero content -->
</ section >
<!-- Main content -->
< main >
<!-- Content -->
</ main >
<!-- Deferred main script -->
< script type = "module" src = "/js/app.js" ></ script >
<!-- Async analytics -->
< script async src = "https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID" ></ script >
<!-- Prefetch next likely page -->
< link rel = "prefetch" href = "/dashboard" />
</ body >
</ html >
Measuring impact
Before optimization
After optimization
Lighthouse Score: 45/100
FCP: 3.2s
LCP: 5.8s
TTI: 7.2s
CLS: 0.15
Render-blocking resources: 4 (2.1s)
- main.css (850ms)
- fonts.css (320ms)
- app.js (680ms)
- analytics.js (250ms)
Lighthouse Score: 98/100
FCP: 0.9s ⬇ 2.3s (72% improvement)
LCP: 1.8s ⬇ 4.0s (69% improvement)
TTI: 2.1s ⬇ 5.1s (71% improvement)
CLS: 0.02 ⬇ 0.13 (87% improvement)
Render-blocking resources: 0 (0s)
- Critical CSS inlined
- Fonts preloaded
- Scripts deferred
- Analytics async
Critical rendering path checklist
Next steps
Profiling tools Use Chrome DevTools and Lighthouse to identify optimization opportunities
Optimization techniques Apply memoization, virtualization, and code splitting for better performance