Render Blocking Resources
Eliminate resources that block page rendering for faster initial paint
Render Blocking Resources
Render-blocking resources prevent the browser from displaying content. Eliminating or deferring them is critical for fast First Contentful Paint (FCP) and Largest Contentful Paint (LCP).
What Blocks Rendering?
Critical Rendering Path
1. HTML parsing
↓
2. CSS blocks rendering (render-blocking)
↓
3. JavaScript blocks parsing (parser-blocking)
↓
4. Render tree construction
↓
5. Layout
↓
6. PaintRender-Blocking Resources
✅ Blocks rendering (bad):
- External CSS (<link rel="stylesheet">)
- Synchronous JavaScript (<script src="...">)
- @import in CSS
- Web fonts (FOIT/FOUT)
❌ Doesn't block:
- Images
- Async/defer scripts
- Media queries (non-matching)
- Lazy-loaded resourcesCSS Optimization
1. Inline Critical CSS
Extract above-the-fold CSS and inline it:
<!DOCTYPE html>
<html>
<head>
<!-- Inline critical CSS -->
<style>
/* Only styles for above-the-fold content */
body { margin: 0; font-family: sans-serif; }
.hero { height: 100vh; background: #000; }
.nav { position: fixed; top: 0; width: 100%; }
</style>
<!-- Defer non-critical CSS -->
<link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles.css"></noscript>
</head>
<body>
<!-- Above-the-fold content renders immediately -->
</body>
</html>2. Extract Critical CSS (Tools)
# Critical (npm package)
npm install critical
# Generate critical CSS
npx critical https://example.com --inline --extract// critical.config.js
const critical = require('critical');
critical.generate({
src: 'dist/index.html',
target: {
html: 'dist/index-critical.html',
css: 'dist/critical.css',
},
inline: true,
extract: true,
width: 1300,
height: 900,
penthouse: {
blockJSRequests: false,
},
});3. Media Queries (Non-Blocking)
<!-- Only blocks if media query matches -->
<link rel="stylesheet" href="/print.css" media="print" />
<link rel="stylesheet" href="/mobile.css" media="(max-width: 640px)" />
<link rel="stylesheet" href="/desktop.css" media="(min-width: 1024px)" />
<!-- Loads all, but doesn't block if doesn't match -->4. Preload + Async Load CSS
<!-- Preload CSS (non-blocking) -->
<link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<!-- Fallback for no-JS -->
<noscript>
<link rel="stylesheet" href="/styles.css">
</noscript>5. Split CSS by Route
// next.config.js
module.exports = {
experimental: {
optimizeCss: true, // Enables CSS optimization
},
};
// Only load CSS for current route
// pages/home.tsx → home.css
// pages/dashboard.tsx → dashboard.cssJavaScript Optimization
1. Async Scripts
<!-- ❌ Blocks parsing -->
<script src="/script.js"></script>
<!-- ✅ Downloads in parallel, doesn't block parsing -->
<script async src="/script.js"></script>
<!-- ✅ Downloads in parallel, executes after parsing -->
<script defer src="/script.js"></script>Async vs Defer
No attribute (blocking):
HTML parsing → STOP → Download + Execute JS → Resume parsing
async:
HTML parsing → Download JS (parallel) → STOP → Execute → Resume
(Executes as soon as downloaded)
defer:
HTML parsing → Download JS (parallel) → Finish parsing → Execute all defer scripts in order
(Executes after HTML parsed, maintains order)2. Defer Non-Critical Scripts
<!DOCTYPE html>
<html>
<head>
<!-- Critical scripts only -->
<script>
// Inline critical code
window.APP_CONFIG = { apiUrl: '/api' };
</script>
</head>
<body>
<!-- Content -->
<!-- Non-critical scripts at end with defer -->
<script defer src="/analytics.js"></script>
<script defer src="/chat-widget.js"></script>
<script defer src="/app.js"></script>
</body>
</html>3. Code Splitting
// Split code by route
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}4. Dynamic Imports
// Load on interaction
async function loadChart() {
const { Chart } = await import('chart.js');
const chart = new Chart(/* ... */);
}
// Load on visibility
const observer = new IntersectionObserver((entries) => {
entries.forEach(async (entry) => {
if (entry.isIntersecting) {
const { VideoPlayer } = await import('./VideoPlayer');
// Render video player
}
});
});Font Optimization
1. font-display: swap
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap; /* Show fallback immediately, swap when loaded */
}
/* Other options:
block: Wait ~3s, then show fallback (❌ FOIT - Flash of Invisible Text)
swap: Show fallback immediately, swap when loaded (✅ Recommended)
fallback: Wait ~100ms, swap if loaded, else show fallback
optional: Use font only if cached
*/2. Preload Critical Fonts
<head>
<!-- Preload critical fonts -->
<link
rel="preload"
href="/fonts/inter-var.woff2"
as="font"
type="font/woff2"
crossorigin
/>
</head>3. Subset Fonts
# Only include characters you use
npx glyphhanger https://example.com --formats=woff2 --subset=fonts/font.ttf
# Result: Font size reduced from 500KB to 50KBNext.js Optimization
1. Next.js Font Optimization
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
preload: true, // Preload font
variable: '--font-inter',
});
export default function RootLayout({ children }) {
return (
<html className={inter.variable}>
<body>{children}</body>
</html>
);
}2. Next.js Script Component
import Script from 'next/script';
export default function Page() {
return (
<>
{/* Critical - loads before page is interactive */}
<Script
src="/critical.js"
strategy="beforeInteractive"
/>
{/* After page interactive (defer) */}
<Script
src="/analytics.js"
strategy="afterInteractive"
/>
{/* Lazy - load when idle */}
<Script
src="/chat-widget.js"
strategy="lazyOnload"
/>
</>
);
}Remove Unused CSS
1. PurgeCSS
npm install @fullhuman/postcss-purgecss
# postcss.config.js
module.exports = {
plugins: [
require('@fullhuman/postcss-purgecss')({
content: [
'./pages/**/*.{js,jsx,ts,tsx}',
'./components/**/*.{js,jsx,ts,tsx}',
],
defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [],
safelist: ['html', 'body'],
}),
],
};2. UnCSS
npm install uncss
# Remove unused CSS
npx uncss https://example.com -o output.css3. Tailwind CSS (Built-in Purge)
// tailwind.config.js
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
// Automatically removes unused classes
};Third-Party Scripts
1. Load Third-Parties After Page Load
// Load analytics after page interactive
useEffect(() => {
if (typeof window !== 'undefined') {
const script = document.createElement('script');
script.src = 'https://www.google-analytics.com/analytics.js';
script.async = true;
document.body.appendChild(script);
}
}, []);2. Partytown (Web Worker)
npm install @builder.io/partytown
# Move third-party scripts to web workerimport { Partytown } from '@builder.io/partytown/react';
export default function App() {
return (
<>
<Partytown debug={true} forward={['dataLayer.push']} />
{/* Third-party scripts run in web worker */}
<script type="text/partytown" src="https://www.googletagmanager.com/gtag/js" />
</>
);
}Measuring Render-Blocking
Chrome DevTools
1. Open DevTools
2. Performance tab
3. Record page load
4. Look for:
- Parse Stylesheet (blocking)
- Evaluate Script (blocking)
- Long Tasks (> 50ms)Lighthouse
npx lighthouse https://example.com --view
# Look for:
# - "Eliminate render-blocking resources"
# - "Reduce unused CSS"
# - "Reduce unused JavaScript"WebPageTest
https://webpagetest.org
Check:
- Start Render time
- First Contentful Paint
- Render-blocking requestsBest Practices
- Inline Critical CSS: Above-the-fold styles only
- Defer Non-Critical CSS: Load async after initial render
- Async/Defer Scripts: Never block parsing
- font-display: swap: Show text immediately
- Preload Fonts: Critical fonts only
- Code Split: Load only what's needed
- Remove Unused CSS: Purge unused styles
- Third-Parties Last: Load after page interactive
- Media Queries: Non-matching CSS doesn't block
- Monitor: Measure with Lighthouse
Real-World Example
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 1. Preconnect to origins -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://cdn.example.com">
<!-- 2. Preload critical resources -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/hero.webp" as="image">
<!-- 3. Inline critical CSS -->
<style>
/* Above-the-fold styles */
body{margin:0;font-family:-apple-system,sans-serif}
.hero{height:100vh;background:#000;color:#fff}
</style>
<!-- 4. Preload then async load full CSS -->
<link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles.css"></noscript>
</head>
<body>
<!-- Content renders immediately -->
<div class="hero">
<h1>Welcome</h1>
</div>
<!-- 5. Defer non-critical scripts -->
<script defer src="/app.js"></script>
<script defer src="/analytics.js"></script>
</body>
</html>Common Pitfalls
❌ All CSS in <head>: Blocks render
✅ Inline critical, async load rest
❌ Sync scripts in <head>: Blocks parsing
✅ async/defer or end of <body>
❌ font-display: block: Invisible text
✅ font-display: swap
❌ Bundling everything: Large initial load
✅ Code splitting by route
❌ Third-parties in <head>: Slow FCP
✅ Load after page interactive
Eliminating render-blocking resources is the single biggest improvement you can make to loading performance—prioritize this optimization first!