Render-blocking resources are CSS files, JavaScript files, and fonts that prevent the browser from painting content to the screen until they've fully loaded and been processed. Eliminating them is one of the highest-impact performance optimizations you can make.
What Are Render-Blocking Resources?
When a browser loads a page, it parses the HTML from top to bottom. When it encounters a <link> to a stylesheet or a <script> tag (without async or defer), it pauses rendering until that resource downloads and executes. Nothing appears on screen until these blocking resources finish.
Default blocking behavior:
| Resource Type | Blocking by Default? | Impact |
|---|---|---|
CSS (<link rel="stylesheet">) | Yes | Blocks all rendering |
JS (<script>) | Yes | Blocks parsing and rendering |
JS (<script defer>) | No | Downloads in parallel, executes after HTML parsing |
JS (<script async>) | No | Downloads in parallel, executes when ready |
| Fonts | Partially | Can delay text paint (FOIT/FOUT) |
The more render-blocking resources in your <head>, the longer users stare at a blank screen.
Identifying Render-Blocking Resources
Using Lighthouse
Run a Lighthouse audit (Chrome DevTools → Lighthouse tab). Look for the "Eliminate render-blocking resources" opportunity. It lists every resource blocking the first paint and the potential time savings.
Using the Performance Panel
- Open Chrome DevTools → Performance tab
- Record a page load
- Look at the waterfall chart for the first ~2 seconds
- Identify CSS and JS requests that complete before First Contentful Paint (FCP)
- These are your render-blocking resources
Using WebPageTest
Run a test at webpagetest.org and examine the waterfall view. Render-blocking resources appear as bars that extend before the "Start Render" marker.
Eliminating Render-Blocking CSS
CSS is the most common render blocker. The browser won't paint anything until it has processed all CSS linked in the <head>. Here's how to fix that without breaking your styles.
Strategy 1: Inline Critical CSS
Extract the CSS needed for above-the-fold content and inline it directly in the HTML <head>. Load the full stylesheet asynchronously.
How it works:
<head>
<!-- Critical CSS inlined -->
<style>
/* Only the CSS needed for above-fold content */
body {
font-family: system-ui;
margin: 0;
}
.hero {
padding: 4rem 2rem;
}
.nav {
display: flex;
height: 64px;
}
</style>
<!-- Full stylesheet loaded async -->
<link
rel="preload"
href="/styles.css"
as="style"
onload="this.onload=null;this.rel='stylesheet'"
/>
<noscript><link rel="stylesheet" href="/styles.css" /></noscript>
</head>
Tools for extracting critical CSS:
- Critical (npm package) — Extracts critical-path CSS automatically
- Critters (Webpack/Vite plugin) — Inlines critical CSS during build
- PurgeCSS — Removes unused CSS to reduce total size
Strategy 2: Split CSS by Route
Instead of one large stylesheet for the entire site, load only the CSS each page needs. Modern bundlers (Webpack, Vite, Next.js) handle this automatically when you use CSS modules or scoped styles.
Strategy 3: Media Query Splitting
Split print styles and large-breakpoint styles into separate files with media attributes. The browser still downloads them but won't block rendering for non-matching media.
<link rel="stylesheet" href="/main.css" />
<link rel="stylesheet" href="/print.css" media="print" />
<link rel="stylesheet" href="/large.css" media="(min-width: 1200px)" />
Eliminating Render-Blocking JavaScript
Use defer for Non-Critical Scripts
The defer attribute tells the browser to download the script in parallel with HTML parsing and execute it after the DOM is ready. Order is preserved.
<!-- Blocks rendering -->
<script src="/analytics.js"></script>
<!-- Non-blocking, executes after DOM ready -->
<script src="/analytics.js" defer></script>
Use defer for scripts that need the DOM but aren't needed for initial render: analytics, third-party widgets, below-fold interactivity.
Use async for Independent Scripts
The async attribute downloads the script in parallel and executes it as soon as it's ready, regardless of DOM state. Execution order is NOT guaranteed.
<script src="/tracking.js" async></script>
Use async for scripts that are independent and don't depend on DOM order: analytics pixels, ad scripts, performance monitoring.
Move Scripts to Bottom
If defer and async aren't options (legacy code), move <script> tags to the bottom of <body>. This ensures HTML content parses and renders before scripts execute.
Code-Split Large Bundles
A 500KB JavaScript bundle blocks the main thread for hundreds of milliseconds even after downloading. Split your code:
- Route-based splitting — Load only the JS needed for the current page
- Component-based splitting — Lazy-load heavy components (charts, editors, maps)
- Dynamic imports —
import()syntax for on-demand loading
Optimizing Font Loading
Web fonts cause either invisible text (FOIT) or a flash of unstyled text (FOUT). Both hurt perceived performance and can contribute to CLS.
Use font-display: swap
@font-face {
font-family: "Custom Font";
src: url("/fonts/custom.woff2") format("woff2");
font-display: swap;
}
swap shows fallback text immediately and swaps to the custom font when loaded. Users see content instantly.
Preload Critical Fonts
<link
rel="preload"
href="/fonts/heading.woff2"
as="font"
type="font/woff2"
crossorigin
/>
Preloading tells the browser to fetch the font early, reducing the time before the font swap happens.
Subset Your Fonts
Most sites use a small subset of characters. Subsetting removes unused glyphs:
- Full Latin font: ~150KB
- Subsetted to English characters: ~20KB
Tools: glyphhanger, FontSquirrel generator, Google Fonts (subsets automatically via &subset=latin)
Self-Host Fonts
Self-hosting fonts eliminates DNS lookups and connection setup to third-party font CDNs. Download fonts, convert to WOFF2, and serve from your own domain.
Measuring the Impact
After implementing changes, measure the improvement:
| Metric | Before | After | Target |
|---|---|---|---|
| FCP (First Contentful Paint) | < 1.8s | ||
| LCP (Largest Contentful Paint) | < 2.5s | ||
| Total blocking time | < 200ms | ||
| Render-blocking resources | 0 |
Run Lighthouse before and after. Use the Performance tab waterfall to verify resources are no longer blocking the render path.
Implementation Checklist
CSS:
- Critical CSS inlined in
<head> - Non-critical CSS loaded asynchronously
- Unused CSS removed (PurgeCSS or equivalent)
- Print and large-viewport styles split by media query
JavaScript:
- All non-essential scripts use
deferorasync - Large bundles code-split by route
- Heavy components lazy-loaded
- Third-party scripts deferred or loaded on interaction
Fonts:
-
font-display: swapapplied to all custom fonts - Critical fonts preloaded
- Fonts subsetted to needed character sets
- Using WOFF2 format
FAQs
Will removing render-blocking resources break my site?
Not if done correctly. The key is ensuring critical CSS (above-the-fold styles) is still available immediately. Moving non-critical CSS and JS to async loading shouldn't affect appearance or functionality.
How many render-blocking resources are normal?
Zero is the target. In practice, one small critical CSS file in the <head> is acceptable. Any JavaScript in the <head> without defer or async is a problem.
Does this affect SEO directly?
Yes. Render-blocking resources slow down FCP and LCP, both of which are Core Web Vitals that Google uses as ranking signals. Faster rendering also helps AI crawlers parse your content more efficiently.
Should I use async or defer for my scripts?
Use defer for scripts that depend on DOM order or other scripts. Use async for independent scripts that can run in any order. When in doubt, use defer—it's safer and more predictable.