View contents
Complete Guide to Eliminating Render-Blocking Resources
Introduction
Render-blocking resources are one of the most impactful performance issues you can fix. By eliminating or minimizing these blockers, you can dramatically improve First Contentful Paint (FCP), Largest Contentful Paint (LCP), and Speed Index.
This guide provides advanced strategies to identify and eliminate render-blocking CSS and JavaScript, based on official Chrome Developers documentation and web.dev best practices.
New to render-blocking resources? Start with our beginner-friendly guide: 📖 Read: Render-Blocking Resources Explained
Understanding the Critical Rendering Path
Before optimizing, understand how browsers render pages:
1. HTML Download & Parse
├── Build DOM (Document Object Model)
└── Discover CSS/JS links
2. CSS Download & Parse
└── Build CSSOM (CSS Object Model)
⚠️ BLOCKING: Nothing renders until complete
3. JavaScript Download & Execute
⚠️ BLOCKING: Halts HTML parsing (unless async/defer)
4. Render Tree Construction
└── DOM + CSSOM = Render Tree
5. Layout & Paint
└── Finally visible to user!
Why CSS Blocks Rendering
The browser cannot render anything until it builds the CSSOM because:
- It doesn’t know what colors, fonts, or sizes to use
- Layout depends on CSS properties
- Rendering without CSS would cause Flash of Unstyled Content (FOUC)
Why JavaScript Blocks Parsing
JavaScript can modify both DOM and CSSOM, so the browser:
- Pauses HTML parsing when it encounters a
<script> - Waits for CSS to load (JS might query styles)
- Executes JS before continuing
Strategy 1: Extract and Inline Critical CSS
Critical CSS is the minimum CSS needed to render above-the-fold content.
Manual Critical CSS Extraction
<head>
<!-- Inline critical CSS -->
<style>
/* Reset and base styles */
* { margin: 0; padding: 0; box-sizing: border-box; }
/* Header styles */
.header {
background: #fff;
height: 64px;
border-bottom: 1px solid #eee;
}
/* Hero section */
.hero {
padding: 4rem 2rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
/* Typography for above-the-fold */
h1 { font-size: 2.5rem; font-weight: 700; }
p { font-size: 1.125rem; line-height: 1.6; }
</style>
<!-- Load full CSS asynchronously -->
<link rel="preload" href="/css/main.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/main.css"></noscript>
</head>
Automated Critical CSS with Tools
Using Critical (npm package)
// build-critical.js
const critical = require('critical');
critical.generate({
inline: true,
base: 'dist/',
src: 'index.html',
target: {
html: 'index-critical.html',
css: 'critical.css'
},
width: 1300,
height: 900,
// Include both mobile and desktop viewports
dimensions: [
{ width: 375, height: 667 }, // Mobile
{ width: 1300, height: 900 } // Desktop
]
});
Using Critters (Webpack/Vite)
// vite.config.js
import critters from 'critters';
export default {
plugins: [
critters({
// Inline critical CSS
preload: 'swap',
// Remove unused CSS
pruneSource: true
})
]
}
Strategy 2: Defer Non-Critical CSS
Preload + onload Pattern
<!-- Preload starts download immediately -->
<link rel="preload" href="/css/styles.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<!-- Fallback for no-JS -->
<noscript>
<link rel="stylesheet" href="/css/styles.css">
</noscript>
Using Media Queries
<!-- Only loads/blocks for matching media -->
<link rel="stylesheet" href="/css/print.css" media="print">
<link rel="stylesheet" href="/css/mobile.css" media="(max-width: 768px)">
<link rel="stylesheet" href="/css/desktop.css" media="(min-width: 769px)">
LoadCSS Pattern
<script>
// loadCSS function for async CSS loading
function loadCSS(href) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
document.head.appendChild(link);
}
// Load non-critical CSS after initial render
if (window.requestIdleCallback) {
requestIdleCallback(() => loadCSS('/css/below-fold.css'));
} else {
setTimeout(() => loadCSS('/css/below-fold.css'), 100);
}
</script>
Strategy 3: Optimize JavaScript Loading
defer vs async
<!-- DEFER: Downloads in parallel, executes after HTML parsing, maintains order -->
<script src="/js/vendor.js" defer></script>
<script src="/js/app.js" defer></script>
<!-- ASYNC: Downloads in parallel, executes immediately when ready, no order -->
<script src="/js/analytics.js" async></script>
When to Use Each
| Pattern | Use Case | Order Guaranteed |
|---|---|---|
defer |
Main app bundle, dependencies | ✅ Yes |
async |
Analytics, ads, independent widgets | ❌ No |
End of <body> |
Legacy fallback | ✅ Yes |
Module Scripts (type=“module”)
Module scripts are deferred by default:
<!-- These are deferred automatically -->
<script type="module" src="/js/app.js"></script>
<script type="module">
import { init } from './app.js';
init();
</script>
Strategy 4: Code Splitting and Lazy Loading
Route-Based Code Splitting
// React
const ProductPage = React.lazy(() => import('./pages/Product'));
// Vue
const ProductPage = () => import('./pages/Product.vue');
// Vanilla JS with dynamic imports
button.addEventListener('click', async () => {
const { openModal } = await import('./modal.js');
openModal();
});
Component-Level Splitting
// React example
import { Suspense, lazy } from 'react';
const HeavyChart = lazy(() => import('./HeavyChart'));
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading chart...</div>}>
<HeavyChart />
</Suspense>
</div>
);
}
Strategy 5: Remove Unused CSS and JavaScript
Finding Unused Code with Coverage
- Open Chrome DevTools (F12)
- Press Ctrl+Shift+P, type “Coverage”
- Click “Start instrumenting coverage and reload”
- Navigate through your site
- Export and analyze the results
PurgeCSS for Unused CSS
// postcss.config.js
const purgecss = require('@fullhuman/postcss-purgecss');
module.exports = {
plugins: [
purgecss({
content: ['./src/**/*.html', './src/**/*.js', './src/**/*.jsx'],
defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [],
safelist: ['active', 'open', /^modal-/]
})
]
};
Tree Shaking in Build Tools
// webpack.config.js
module.exports = {
mode: 'production', // Enables tree shaking
optimization: {
usedExports: true,
sideEffects: true
}
};
Strategy 6: Optimize Third-Party Scripts
Third-party scripts are often the worst offenders for render blocking.
Delay Until User Interaction
// Load chat widget only on first interaction
let chatLoaded = false;
function loadChat() {
if (chatLoaded) return;
chatLoaded = true;
const script = document.createElement('script');
script.src = 'https://chat-widget.example.com/widget.js';
document.body.appendChild(script);
}
// Load on any user interaction
['click', 'scroll', 'keydown', 'touchstart'].forEach(event => {
document.addEventListener(event, loadChat, { once: true, passive: true });
});
// Or after 5 seconds as fallback
setTimeout(loadChat, 5000);
Use Facade Pattern
<!-- Show static placeholder until interaction -->
<div class="youtube-placeholder" data-video-id="abc123">
<img src="/images/video-thumbnail.jpg" alt="Video thumbnail">
<button class="play-button" aria-label="Play video">▶</button>
</div>
<script>
document.querySelector('.youtube-placeholder').addEventListener('click', function() {
const videoId = this.dataset.videoId;
this.outerHTML = `
<iframe
src="https://www.youtube.com/embed/${videoId}?autoplay=1"
allow="autoplay; encrypted-media"
allowfullscreen>
</iframe>
`;
});
</script>
Self-Host Critical Third-Party Resources
<!-- Instead of external CDN -->
<link href="https://fonts.googleapis.com/css2?family=Roboto" rel="stylesheet">
<!-- Self-host for better control -->
<style>
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto.woff2') format('woff2');
font-display: swap;
}
</style>
Measuring Improvements
Before/After Comparison
BEFORE:
├── Render-blocking resources: 5
├── FCP: 3.8s
├── LCP: 5.2s
├── Speed Index: 4.5s
└── Lighthouse Score: 42
AFTER:
├── Render-blocking resources: 0
├── FCP: 1.2s (↓68%)
├── LCP: 2.1s (↓60%)
├── Speed Index: 1.8s (↓60%)
└── Lighthouse Score: 94
Lighthouse CI for Continuous Monitoring
# .github/workflows/lighthouse.yml
- name: Run Lighthouse
uses: treosh/lighthouse-ci-action@v9
with:
urls: https://example.com/
budgetPath: ./budget.json
// budget.json
{
"resourceSizes": [
{ "resourceType": "script", "budget": 150 },
{ "resourceType": "stylesheet", "budget": 50 }
],
"resourceCounts": [
{ "resourceType": "third-party", "budget": 10 }
]
}
Common Pitfalls and Solutions
Pitfall 1: CSS Framework Bloat
<!-- ❌ Loading entire Bootstrap -->
<link href="bootstrap.min.css" rel="stylesheet">
<!-- ✅ Import only what you need -->
<style>
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "bootstrap/scss/grid";
/* Only grid utilities, nothing else */
</style>
Pitfall 2: Font Loading Flash
/* ❌ No font-display causes invisible text */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2');
}
/* ✅ Show fallback immediately */
@font-face {
font-family: 'CustomFont';
src: url('font.woff2');
font-display: swap;
}
Pitfall 3: Inline Scripts Blocking
<!-- ❌ Inline script blocks parsing -->
<script>
// Heavy computation here
</script>
<!-- ✅ Move to external file with defer -->
<script src="computation.js" defer></script>
Optimization Checklist
Before deploying, verify:
- [ ] Critical CSS is inlined in
<head> - [ ] Non-critical CSS loads asynchronously
- [ ] All
<script>tags usedeferorasync - [ ] Third-party scripts are delayed or use facades
- [ ] Unused CSS is removed (PurgeCSS or similar)
- [ ] JavaScript is code-split by route
- [ ] Coverage tab shows minimal unused code
- [ ] Lighthouse shows 0 render-blocking resources
- [ ] FCP < 1.8s and LCP < 2.5s
Related Articles
Continue optimizing your site’s rendering performance:
- Render-Blocking Resources Explained - Understand the basics
- Speed Index Optimization Guide - Improve visual progress
- FCP Optimization Guide - Speed up first paint
- LCP Optimization Guide - Optimize largest content
📚 Back to Performance SEO Hub - Explore all performance topics
References
- Chrome Developers - Eliminate Render-Blocking Resources
- web.dev - Critical Rendering Path
- web.dev - Render-Blocking CSS
- web.dev - Defer Non-Critical CSS
Try It Yourself
Ready to eliminate render-blocking resources?
🔧 Download UXR SEO Analyzer (Free, 100% local analysis)
Disclaimer: The analyzers in this extension are reference guides based on official Chrome Developers and web.dev documentation. They do not represent absolute truths about how search engines evaluate your content—only search engines know their internal algorithms. Use these recommendations as a starting point to improve your site.
Last updated: December 14, 2025