Optimizing the critical rendering path for instant loading
Understanding how browsers process and render web pages
HTML parser creates Document Object Model
CSS parser creates CSS Object Model
Combine visible elements with styles
Calculate element positions and sizes
Render pixels and composite layers
Blocks DOM construction
Cannot render without DOM
Blocks first render
Above-the-fold styles
Stops HTML parsing
Script without async/defer
Extracting and inlining critical CSS for faster first render
// Critical CSS tools
npm install --save-dev critical
// Usage in build process
const critical = require('critical')
critical.generate({
base: 'dist/',
src: 'index.html',
dest: 'index-critical.html',
width: 1300,
height: 900,
minify: true,
extract: true,
inlineImages: false,
dimensions: [
{ width: 320, height: 480 }, // Mobile
{ width: 768, height: 1024 }, // Tablet
{ width: 1300, height: 900 } // Desktop
]
})
// Webpack plugin
const HtmlWebpackPlugin = require('html-webpack-plugin')
const HtmlCriticalWebpackPlugin = require('html-critical-webpack-plugin')
module.exports = {
plugins: [
new HtmlWebpackPlugin(),
new HtmlCriticalWebpackPlugin({
base: path.resolve(__dirname, 'dist'),
src: 'index.html',
dest: 'index.html',
inline: true,
minify: true,
extract: true,
width: 375,
height: 565,
penthouse: { blockJSRequests: false }
})
]
}<!-- Inline critical CSS -->
<style>
/* Critical above-the-fold CSS */
.header { display: block; }
.hero { height: 100vh; }
.fold { margin-bottom: 2rem; }
</style>
<!-- Preload non-critical CSS -->
<link rel="preload" href="/styles/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles/main.css"></noscript>
<!-- Async CSS loading function -->
<script>
function loadCSS(href) {
const link = document.createElement('link')
link.rel = 'stylesheet'
link.href = href
link.media = 'print'
link.onload = () => { link.media = 'all' }
document.head.appendChild(link)
}
// Load non-critical CSS
loadCSS('/styles/components.css')
loadCSS('/styles/pages.css')
</script>
<!-- Resource hints -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="dns-prefetch" href="//cdn.example.com">Optimizing JavaScript to avoid blocking the critical rendering path
<!-- Different JavaScript loading strategies -->
<!-- ❌ Blocking script (blocks HTML parsing) -->
<script src="blocking-script.js"></script>
<!-- ✅ Async script (downloads in parallel, executes when ready) -->
<script async src="analytics.js"></script>
<!-- ✅ Defer script (downloads in parallel, executes after DOM ready) -->
<script defer src="app.js"></script>
<!-- ✅ Module script (modern browsers, automatic defer) -->
<script type="module" src="modern.js"></script>
<script nomodule src="legacy.js"></script>
<!-- ✅ Dynamic import (load on demand) -->
<script>
// Load feature when needed
document.getElementById('feature-button').addEventListener('click', async () => {
const { initFeature } = await import('./feature.js')
initFeature()
})
// Preload critical JavaScript
const link = document.createElement('link')
link.rel = 'preload'
link.as = 'script'
link.href = '/critical.js'
document.head.appendChild(link)
</script>
<!-- ✅ Service Worker for caching -->
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
}
</script>Using browser hints to optimize resource loading priority
<!-- Resource hints for better loading -->
<!-- Preconnect to external domains -->
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
<link rel="preconnect" href="https://api.example.com" crossorigin>
<!-- DNS prefetch for late-discovered resources -->
<link rel="dns-prefetch" href="//cdn.example.com">
<link rel="dns-prefetch" href="//analytics.google.com">
<!-- Preload critical resources -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/hero-image.webp" as="image">
<link rel="preload" href="/critical.css" as="style">
<link rel="preload" href="/app.js" as="script">
<!-- Prefetch likely next resources -->
<link rel="prefetch" href="/page2.html">
<link rel="prefetch" href="/components.js">
<!-- Module preload for ES modules -->
<link rel="modulepreload" href="/modules/app.js">
<!-- Fetch priority (experimental) -->
<script fetchpriority="high" src="/critical.js"></script>
<img src="/hero.jpg" fetchpriority="high" alt="Hero">
<img src="/secondary.jpg" fetchpriority="low" alt="Secondary">
<!-- Resource hints in JavaScript -->
const link = document.createElement('link')
link.rel = 'preload'
link.href = '/dynamic-resource.js'
link.as = 'script'
document.head.appendChild(link)Common patterns for optimizing the critical rendering path
Critical resources for initial route
Initial route as soon as possible
Remaining routes using Service Worker
Create remaining routes on demand
Shell cached, content dynamic
Tools and metrics for measuring critical rendering path performance
// Critical metrics measurement
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
switch (entry.entryType) {
case 'paint':
if (entry.name === 'first-contentful-paint') {
console.log('FCP:', entry.startTime)
}
break
case 'largest-contentful-paint':
console.log('LCP:', entry.startTime)
break
case 'layout-shift':
console.log('CLS:', entry.value)
break
}
}
})
observer.observe({
entryTypes: ['paint', 'largest-contentful-paint', 'layout-shift']
})
// Critical resource timing
window.addEventListener('load', () => {
const resources = performance.getEntriesByType('resource')
// Identify render-blocking resources
const blockingResources = resources.filter(resource => {
return resource.renderBlockingStatus === 'blocking'
})
console.log('Blocking resources:', blockingResources.length)
// Measure critical path metrics
const navigation = performance.getEntriesByType('navigation')[0]
const domComplete = navigation.domComplete
const loadComplete = navigation.loadEventEnd
console.log('Critical path time:', domComplete)
console.log('Full load time:', loadComplete)
})
// Lighthouse programmatic audit
const lighthouse = require('lighthouse')
const chromeLauncher = require('chrome-launcher')
async function auditCriticalPath(url) {
const chrome = await chromeLauncher.launch()
const options = { port: chrome.port }
const result = await lighthouse(url, options)
const audits = result.lhr.audits
return {
fcp: audits['first-contentful-paint'].numericValue,
lcp: audits['largest-contentful-paint'].numericValue,
renderBlocking: audits['render-blocking-resources'].score,
unusedCSS: audits['unused-css-rules'].score,
criticalRequest: audits['critical-request-chains'].displayValue
}
}Complete checklist for critical rendering path optimization