Render-blocking resources prevent the browser from painting anything—even when your LCP element is fully downloaded and ready. The median website loads 7 render-blocking resources totaling 100KB+, adding 500-1500ms to LCP on typical mobile connections.
Browsers follow a strict rule: no pixels appear on screen until all render-blocking resources finish loading. CSS is render-blocking by default because the browser needs styles to know what to paint. Synchronous JavaScript is parser-blocking, which halts HTML processing until the script executes.
The impact is severe. Your LCP image might download in 200ms, but if 300KB of CSS is still loading, users see nothing. The browser has the content; it refuses to display it. This creates an invisible delay that compounds with every additional blocking resource in the critical path.
Lighthouse flags this as "Eliminate render-blocking resources" and calculates the potential time savings. The audit identifies CSS files loaded in the <head> without media attributes and JavaScript files without async or defer. Each blocking resource in the chain extends the time before First Contentful Paint and LCP.
npx lighthouse https://your-site.com --only-audits=render-blocking-resources --output=json | jq '.audits["render-blocking-resources"]'
curl -sI https://your-site.com/styles.css | grep -i content-length
Look for "Eliminate render-blocking resources" under Opportunities. Lighthouse shows:
The audit calculates savings based on how much faster FCP and LCP could occur if resources loaded asynchronously.
Never load JavaScript synchronously in the <head> unless absolutely necessary.
<!-- Before: blocking -->
<head>
<script src="analytics.js"></script>
<script src="app.js"></script>
</head>
<!-- After: non-blocking -->
<head>
<script src="analytics.js" async></script>
<script src="app.js" defer></script>
</head>
Choosing between async and defer:
<!-- async: downloads in parallel, executes immediately when ready -->
<!-- Use for independent scripts like analytics, ads -->
<script src="analytics.js" async></script>
<!-- defer: downloads in parallel, executes after HTML parsing, maintains order -->
<!-- Use for app code that depends on DOM or other scripts -->
<script src="vendor.js" defer></script>
<script src="app.js" defer></script>
For inline scripts that can run later:
<script>
// Defer execution until after DOMContentLoaded
document.addEventListener('DOMContentLoaded', () => {
initializeNonCriticalFeatures()
})
</script>
Extract only the CSS needed for above-the-fold content and inline it directly in the HTML.
<head>
<!-- Critical CSS inlined -->
<style>
/* Only styles for above-the-fold content */
:root { --primary: #0066cc; }
body { margin: 0; font-family: system-ui; }
.header { height: 60px; background: var(--primary); }
.hero { padding: 2rem; }
.hero-image { width: 100%; height: auto; }
</style>
<!-- Full CSS loaded asynchronously -->
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>
</head>
Automate critical CSS extraction:
npx critical https://your-site.com --inline --minify > index-critical.html
npm install critters-webpack-plugin --save-dev
// webpack.config.js
const Critters = require('critters-webpack-plugin')
module.exports = {
plugins: [
new Critters({
preload: 'swap',
inlineThreshold: 0,
}),
],
}
Web fonts can block text rendering for up to 3 seconds on slow connections. Use font-display: swap to show fallback text immediately.
@font-face {
font-family: 'Custom Font';
src: url('/fonts/custom.woff2') format('woff2');
font-display: swap;
}
Preload critical fonts:
<head>
<link rel="preload" href="/fonts/custom.woff2" as="font" type="font/woff2" crossorigin>
</head>
For Google Fonts, add the display=swap parameter:
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
Break monolithic CSS into route-specific chunks:
<!-- Load base styles everywhere -->
<link rel="stylesheet" href="base.css">
<!-- Load page-specific styles only where needed -->
<link rel="stylesheet" href="product-page.css" media="print" onload="this.media='all'">
// Dynamic CSS loading in JavaScript
document.getElementById('modal-trigger').addEventListener('click', () => {
import('./modal.css')
showModal()
})
Eliminating render-blocking resources allows the browser to start painting immediately after critical CSS is processed. The browser no longer waits for external stylesheets or JavaScript before displaying content. With async/defer scripts, JavaScript downloads in parallel without blocking HTML parsing. With inlined critical CSS, the browser has everything it needs to paint above-the-fold content in the initial HTML response. This removes artificial delays between resource availability and visual rendering.
optimizeCss in next.config.js for automatic critical CSS extraction.// next.config.js
module.exports = {
experimental: {
optimizeCss: true,
},
}
<style scoped>. Configure font loading with @nuxt/fonts for optimal font-display settings.// nuxt.config.ts
export default defineNuxtConfig({
experimental: {
inlineSSRStyles: true,
},
fonts: {
defaults: {
display: 'swap',
},
},
})
After implementing changes:
Expected improvement: Removing 200KB of render-blocking CSS and deferring 100KB of JavaScript typically saves 300-800ms on LCP for 4G mobile connections.
Inlining too much CSS: Critical CSS should be 14KB or less (fits in first TCP roundtrip). Inline only above-the-fold styles; load the rest asynchronously. Excessive inline CSS increases HTML size and hurts TTFB.
Breaking functionality with defer: Some legacy scripts expect synchronous execution and access to DOM elements that haven't parsed yet. Test thoroughly after adding defer. Use DOMContentLoaded listeners instead of relying on script position.
Missing fallback fonts: Using font-display: swap without specifying fallback fonts causes layout shift when custom fonts load. Define fallback font stacks that closely match your custom font metrics:
font-family: 'Custom Font', system-ui, -apple-system, sans-serif;
Render-blocking resources often combine with other LCP problems:
Different page templates have different blocking resources. Your blog might inline critical CSS perfectly while your product pages load three external stylesheets. Run a comprehensive audit to find render-blocking issues across every template.
Scan Your Site with Unlighthouse