How to Eliminate Render-Blocking Resources
How to Eliminate Render-Blocking Resources (Complete Guide)
Render-blocking resources are CSS and JavaScript files that prevent the browser from displaying anything on screen until they finish downloading and processing. Eliminating them is one of the most impactful LCP fixes available. The core fixes are: inline critical CSS, defer non-critical JavaScript, and async third-party scripts. Most sites can eliminate render-blocking resources without a developer using free tools and plugins.
Every millisecond your browser spends waiting for a CSS or JavaScript file to load is a millisecond your LCP element cannot paint. Users stare at a blank white screen. Google records a slow LCP timestamp.
Render-blocking resources are flagged in nearly every PageSpeed Insights report. They are one of the most common reasons sites fail LCP, even after fixing images, server response time, and preloading.
This guide covers exactly what render-blocking resources are, how to identify them, and every fix — from beginner-friendly plugin solutions to developer-level implementation.
If you are still working out which Core Web Vitals are failing on your site, start with the complete Core Web Vitals fix guide first.
What are render-blocking resources?
When a browser loads a page, it builds two trees before it can display anything:
- DOM tree — built from parsing your HTML
- CSSOM tree — built from parsing your CSS
The browser cannot render anything until both trees are complete. This is called the Critical Rendering Path.
CSS is render-blocking by default. Every <link rel="stylesheet"> tag the browser encounters in your <head> stops HTML parsing until that CSS file is fully downloaded and parsed. Only then does the browser continue building the DOM and eventually paint the page.
JavaScript is also render-blocking by default when placed in <head> without defer or async. When the browser encounters a <script> tag, it stops everything — stops parsing HTML, stops building the DOM — downloads the script, executes it, and only then continues.
The result looks like this on a typical page:
The browser receives HTML
→ Discovers <link> for styles.css (500KB) → STOPS → downloads → parses
→ Discovers <link> for fonts.css (20KB) → STOPS → downloads → parses
→ Discovers <script> for jquery.min.js (90KB) → STOPS → downloads → executes
→ Discovers <script> for theme.js (200KB) → STOPS → downloads → executes
→ Finally starts building the visible page
→ LCP element discovered and loadedOn a slow mobile connection, this sequence can take 2–4 seconds before the browser even starts working on visible content. Your LCP timestamp does not start until the LCP element is visible — but the clock is running the entire time.
How to identify your render-blocking resources
Method 1: PageSpeed Insights
- Go to pagespeed.web.dev
- Run your URL
- In the Opportunities section, look for "Eliminate render-blocking resources."
- Click to expand — it lists every render-blocking file with its size and estimated time saving
This is the fastest starting point. The estimated savings next to each file tell you which ones to prioritise.
Method 2: Chrome DevTools Coverage panel
- Open Chrome DevTools (F12)
- Press Ctrl+Shift+P and type Coverage
- Click Start instrumenting coverage and reload the page
- After the page loads, the Coverage panel shows every CSS and JavaScript file with a percentage of unused code
Files with 70%+ unused code on the initial page load are strong candidates for deferring or splitting. You are making users download code they do not need for the initial render.
Method 3: Chrome DevTools Performance panel
- Open the Performance panel
- Record a page load
- In the waterfall, look for long bars in the Main thread during the early load phases
- Hover over purple bars labelled Parse Stylesheet and yellow bars labelled Evaluate Script — these show exactly which files are blocking rendering and for how long
Fix 1: Inline critical CSS
Critical CSS is the minimum CSS required to render the above-the-fold content — everything visible without scrolling. Instead of making the browser wait for an external CSS file, you embed this CSS directly in a <style> tag in your <head>.
The result: the browser can render above-the-fold content immediately after receiving the HTML, without waiting for any external file.
What critical CSS looks like:
html
<head>
<style>
/* Critical CSS — inlined directly */
body { margin: 0; font-family: Arial, sans-serif; }
header { background: #1a1a2e; padding: 1rem 2rem; }
.hero { width: 100%; height: 500px; position: relative; }
.hero img { width: 100%; height: 100%; object-fit: cover; }
h1 { font-size: 2.5rem; color: #ffffff; margin: 0; }
</style>
<!-- Non-critical CSS loads after -->
<link rel="stylesheet" href="/css/styles.css" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="/css/styles.css"></noscript>
</head>The media="print" trick loads the full CSS file asynchronously. The browser downloads it in the background (print stylesheets do not block rendering), and the onload handler switches it to media="all" when it finishes. The <noscript> fallback handles browsers with JavaScript disabled.
How to generate your critical CSS:
Manually writing critical CSS is time-consuming and error-prone. Use automated tools:
- Critical (npm package) —
npm install -g critical— the most widely used tool, integrates with build pipelines - Penthouse — generates critical CSS from a URL, works without a build process
- CriticalCSS — similar to Penthouse, good for one-off generation
- Critters (Webpack/Next.js plugin) — automatically inlines critical CSS during the build process
For a one-off generation without any build tools, paste your URL into jonassebastianohlsson.com/criticalpathcssgenerator — it generates critical CSS you can copy and paste directly into your theme.
How much critical CSS to inline:
Keep inlined critical CSS under 14KB (uncompressed). This is the size of a single TCP packet — anything under 14KB is sent in the very first response from the server alongside the HTML. Larger critical CSS still helps, but loses the "first packet" advantage.
Fix 2: Defer non-critical JavaScript
JavaScript files in <head> without defer or They async are the most common render-blocking culprits on most websites.
The three ways to load JavaScript:
html
<!-- 1. Synchronous — BLOCKS rendering (default, bad for <head>) -->
<script src="/js/script.js"></script>
<!-- 2. Defer — downloads in parallel, executes after HTML is parsed -->
<script src="/js/script.js" defer></script>
<!-- 3. Async — downloads in parallel, executes immediately when ready -->
<script src="/js/script.js" async></script>When to use defer:
Use defer for scripts that need the DOM to be ready before they execute. Deferred scripts:
- Download in parallel with HTML parsing (does not block)
- Execute in order after HTML parsing is complete
- Execute before the
DOMContentLoadedevent
<!-- Good candidates for defer -->
<script src="/js/main.js" defer></script>
<script src="/js/slider.js" defer></script>
<script src="/js/contact-form.js" defer></script>When to use async:
Use async for independent scripts that do not depend on other scripts or the DOM. Async scripts:
- Download in parallel with HTML parsing (does not block)
- Execute immediately when downloaded — in any order
- May execute before or after the DOM is ready
<!-- Good candidates for async -->
<script src="https://www.googletagmanager.com/gtag/js" async></script>
<script src="https://connect.facebook.net/fbevents.js" async></script>Scripts that should NOT be deferred:
Some scripts must remain synchronous because other scripts depend on them or because they modify the DOM before rendering:
- Scripts that are required by inline
<script>blocks that run immediately - A/B testing scripts that need to modify content before the page paints (to avoid flicker)
- Scripts that detect browser capabilities and apply classes to
<html>before render
For everything else — analytics, marketing pixels, chat widgets, social share buttons, comment systems — defer or It async is safe and correct.
Fix 3: Load third-party scripts asynchronously
Third-party scripts are the most common source of render-blocking JavaScript. Analytics platforms, advertising networks, chat widgets, heatmap tools, and social media embeds all inject scripts into your pages — and most of them default to synchronous loading.
The problem is compounded by the fact that third-party scripts load from external servers. The browser has to:
- Perform a DNS lookup for the third-party domain
- Establish a TCP connection
- Perform a TLS handshake (for HTTPS)
- Download the script
- Execute it
Each of these steps takes time — and if the third-party server is slow or experiencing issues, your entire page is held hostage until it responds.
Always load third-party scripts with async or defer:
<!-- Google Analytics 4 -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<!-- Facebook Pixel -->
<script defer src="https://connect.facebook.net/en_US/fbevents.js"></script>
<!-- Hotjar -->
<script defer src="https://static.hotjar.com/c/hotjar-XXXXXXX.js"></script>For maximum performance — load after page interaction:
For non-essential third-party scripts (chat widgets, social proof tools, upsell popups), delay loading until the user first interacts with the page:
// Load script only after first user interaction
function loadThirdPartyScript() {
const script = document.createElement('script');
script.src = 'https://widget.example.com/widget.js';
script.async = true;
document.head.appendChild(script);
// Remove listeners after first interaction
['mousedown', 'touchstart', 'keydown', 'scroll'].forEach(event => {
document.removeEventListener(event, loadThirdPartyScript);
});
}
['mousedown', 'touchstart', 'keydown', 'scroll'].forEach(event => {
document.addEventListener(event, loadThirdPartyScript, { passive: true });
});This pattern is particularly effective for chat widgets. Users who never scroll or interact never load the chat widget at all — saving bandwidth and eliminating the render-blocking impact entirely.
Fix 4: Preconnect to critical third-party origins
Even when third-party scripts are deferred or async, the DNS lookup, TCP connection, and TLS handshake still take time when the script eventually loads. You can start these connection steps early — before the script is even requested — using preconnect:
<head>
<!-- Start connections to critical third-party domains early -->
<link rel="preconnect" href="https://www.googletagmanager.com">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://cdn.example.com">
</head>preconnect tells the browser: "I am going to need resources from this domain soon — start the connection now." The browser performs the DNS lookup, TCP handshake, and TLS negotiation in the background. When the actual resource request fires, the connection is already established and the resource loads immediately.
Use preconnect sparingly — limit to 3–4 critical domains. Every preconnect consumes browser resources. For lower-priority third-party domains, use dns-prefetch instead:
<!-- dns-prefetch — only resolves DNS, lower resource cost -->
<link rel="dns-prefetch" href="https://www.google-analytics.com">
<link rel="dns-prefetch" href="https://widget.intercom.io">Fix 5: Split and tree-shake your JavaScript bundles
If you have a custom JavaScript build process (Webpack, Vite, Rollup, Parcel), large monolithic JavaScript bundles are a major render-blocking source.
Code splitting divides your JavaScript into smaller chunks that load on demand:
// Before — loads entire module upfront
import { HeavyComponent } from './HeavyComponent';
// After — loads only when needed
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));Tree shaking removes unused code from your bundles during the build process. Make sure your bundler has tree shaking enabled:
// webpack.config.js
module.exports = {
mode: 'production', // enables tree shaking automatically
optimization: {
usedExports: true,
splitChunks: {
chunks: 'all', // enables automatic code splitting
}
}
};Analyse your bundle size:
Use bundlephobia.com to check the size of npm packages before adding them. Use webpack-bundle-analyzer to visualise what is inside your existing bundles and identify oversized dependencies.
Common oversized dependencies that can be replaced with lighter alternatives:
| Heavy Library | Size | Lighter Alternative | Size |
|---|---|---|---|
| Moment.js | 67KB | Day.js | 2KB |
| Lodash (full) | 72KB | Lodash (cherry-picked) | 1–5KB |
| jQuery | 87KB | Vanilla JS | 0KB |
| Bootstrap JS | 59KB | Alpine.js | 8KB |
| Font Awesome (full) | 160KB | SVG icons (selected) | 1–10KB |
Replacing Moment.js with Day.js alone saves 65KB of JavaScript — more than enough to move a failing LCP to a passing one on mobile connections.
Fix 6: Optimise Google Fonts loading
Google Fonts is one of the most common render-blocking resources on the web. The default implementation blocks rendering:
<!-- Default Google Fonts — render blocking -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">The optimised implementation:
<head>
<!-- Step 1: Preconnect to Google Fonts domains -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Step 2: Load the CSS asynchronously -->
<link
rel="preload"
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap"
as="style"
onload="this.rel='stylesheet'"
>
<noscript>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
</noscript>
</head>Even better — self-host your fonts:
Download your Google Fonts and serve them from your own server. This eliminates the third-party DNS lookup entirely and gives you full control over caching headers:
- Go to google-webfonts-helper.herokuapp.com
- Find your font and download the WOFF2 files
- Upload to your server
- Reference with
@font-facein your CSS
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: optional;
}Self-hosting fonts also helps with CLS — no third-party latency means fonts load faster and the swap window is shorter. For a full guide to font-related CLS, see what causes cumulative layout shift.
Platform-specific implementation
WordPress
WordPress is where render-blocking resources are most prevalent — themes and plugins routinely add dozens of CSS and JavaScript files to every page.
WP Rocket (easiest, recommended):
- Go to WP Rocket → File Optimisation
- Enable "Minify CSS files" and "Combine CSS files."
- Enable "Load CSS asynchronously" — this automatically applies the critical CSS inline approach
- Enable "Minify JavaScript files."
- Enable "Load JavaScript deferred"
- In the exclusions box, add any scripts that must remain synchronous
WP Rocket handles critical CSS extraction, async CSS loading, and JavaScript deferral automatically. It is the fastest way to eliminate render-blocking resources in WordPress.
LiteSpeed Cache (free alternative):
- Go to LiteSpeed Cache → Page Optimisation → CSS Settings
- Enable CSS Minify, CSS Combine, and CSS HTTP/2 Push
- Go to JS Settings
- Enable JS Minify, JS Combine, and JS Defer
Manual WordPress approach (no plugin):
Add this to your theme's functions.php to defer non-essential scripts:
php
function defer_non_critical_scripts($tag, $handle, $src) {
$defer_scripts = ['slider-js', 'contact-form-js', 'social-share-js'];
if (in_array($handle, $defer_scripts)) {
return '<script src="' . $src . '" defer></script>';
}
return $tag;
}
add_filter('script_loader_tag', 'defer_non_critical_scripts', 10, 3);Replace $defer_scripts with the handles of your non-critical scripts (find these in your browser's Network panel or using the Query Monitor plugin).
Shopify
Shopify's theme architecture makes render-blocking resources harder to fix than WordPress, but the core principles are the same.
In your theme.liquid:
Find all <script> tags in the <head> and add defer to those that do not need to run before the DOM is ready:
<!-- Before -->
<script src="{{ 'theme.js' | asset_url }}"></script>
<!-- After -->
<script src="{{ 'theme.js' | asset_url }}" defer></script>For third-party scripts added via Shopify's Script Editor or theme settings:
<script
src="https://third-party-service.com/script.js"
async
defer
></script>For CSS: Shopify themes typically have one main CSS file. If it is large, consider splitting it and using the media attribute to load non-critical sections asynchronously:
<link rel="stylesheet" href="{{ 'critical.css' | asset_url }}">
<link rel="stylesheet" href="{{ 'non-critical.css' | asset_url }}" media="print" onload="this.media='all'">Next.js
Next.js has built-in script optimisation through the <Script> component:
import Script from 'next/script';
export default function Layout({ children }) {
return (
<>
{/* Strategy: beforeInteractive — blocks until loaded (use sparingly) */}
<Script src="/critical-script.js" strategy="beforeInteractive" />
{/* Strategy: afterInteractive — loads after page is interactive (default) */}
<Script src="https://www.googletagmanager.com/gtag/js" strategy="afterInteractive" />
{/* Strategy: lazyOnload — loads during idle time */}
<Script src="https://widget.intercom.io/widget.js" strategy="lazyOnload" />
{children}
</>
);
}Use strategy="afterInteractive" for analytics and marketing scripts. Use strategy="lazyOnload" for chat widgets, social proof tools, and other non-critical embeds. Never use strategy="beforeInteractive" unless the script is absolutely required before the page renders.
For CSS in Next.js, use CSS Modules or styled-components with SSR to ensure only the CSS needed for the current page is included in the initial render.
How render-blocking resources connect to your other Core Web Vitals
Render-blocking resources primarily affect LCP — but their impact reaches across all three vitals.
Render-blocking → LCP: The most direct connection. Every render-blocking resource delays the point at which the browser can paint the LCP element. Eliminating render-blocking resources is often the difference between a 2.3s and a 3.1s LCP. Combined with preloading your LCP image and reducing server response time, eliminating render-blocking resources completes the three-fix LCP stack that resolves most LCP score failures.
Render-blocking → CLS: JavaScript that runs during page load and modifies the DOM causes layout shifts. Deferring these scripts until after the initial paint prevents them from causing shifts during the critical load window. See what causes cumulative layout shift for the full breakdown.
Render-blocking → INP: Large JavaScript bundles that take a long time to parse and execute create long tasks on the main thread. Long tasks block user interactions. Splitting and deferring JavaScript directly improves INP by reducing main thread contention. This connects closely to what is a good LCP score — a page that loads fast and responds fast is addressing both LCP and INP simultaneously.
Render-blocking resources checklist
| Fix | Effort | Impact | Works Without a Developer |
|---|---|---|---|
| Add defer to the theme JavaScript | Low | High | Yes |
| Add async to analytics scripts | Low | High | Yes |
| Load Google Fonts asynchronously | Low | Medium | Yes |
| Preconnect to critical third-party origins | Low | Medium | Yes |
| Inline critical CSS | Medium | Very high | Yes (with tools) |
| Self-host Google Fonts | Medium | Medium | Yes |
| Delay third-party scripts until interaction | Medium | High | Yes (with snippet) |
| Split JavaScript bundles | High | Very high | Requires developer |
| Replace heavy libraries with lighter alternatives | High | High | Requires developer |
Frequently asked questions
Q1. What are render-blocking resources in PageSpeed Insights?
Render-blocking resources are CSS and JavaScript files that prevent the browser from displaying page content until they finish downloading and processing. PageSpeed Insights flags them under "Eliminate render-blocking resources" in the Opportunities section, with an estimated time saving for each file.
Q2. Does deferring JavaScript break my website?
It can if done incorrectly. Scripts that other scripts depend on must maintain their load order — use defer rather than async for dependent scripts, since defer preserves execution order. Scripts that modify the DOM before the page renders (A/B testing tools, personalisation scripts) may cause visual flicker if deferred. Test thoroughly after implementing deferred loading.
Q3. Should I use defer or async for Google Analytics?
Use async for the Google Analytics or Google Tag Manager script. GA4 is designed to work asynchronously and does not depend on the DOM being ready. The async attribute is correct and safe for all major analytics platforms.
Q4. Is render-blocking CSS always bad?
CSS is render-blocking by design — the browser needs styles to render content correctly. The goal is not to eliminate CSS loading, but to ensure only the CSS needed for the initial render is blocking. Non-critical CSS (styles for below-the-fold content, print styles, dark mode styles) should load asynchronously.
Q5. How do I know if my critical CSS implementation is working?
Run PageSpeed Insights before and after. The "Eliminate render-blocking resources" opportunity should show reduced or eliminated blocking time for your CSS files. In Chrome DevTools Performance panel, look for a reduction in the time between the first HTML response and the first paint event.
Q6. My WordPress plugin adds render-blocking scripts. What do I do?
Use WP Rocket or LiteSpeed Cache's JavaScript deferral feature — these plugins can defer scripts added by other plugins without you needing to modify any plugin code. If a specific plugin's scripts cannot be deferred without breaking functionality, contact the plugin developer or look for a performance-focused alternative.
Q7. How many render-blocking resources are too many?
Any synchronous script in <head> is technically too many — all scripts in <head> should have defer or async unless they are genuinely required before rendering. For CSS, aim to have one small inlined critical CSS block and one asynchronously-loaded full stylesheet. Anything beyond this should be evaluated for necessity.
Summary
Render-blocking resources are CSS and JavaScript files that delay everything — LCP, first paint, interactivity. The fix strategy in priority order:
- Inline critical CSS — eliminates CSS render-blocking entirely for above-the-fold content
- Defer non-critical JavaScript — add
deferto theme and plugin scripts - Async third-party scripts — add
asyncto analytics, pixels, and marketing scripts - Preconnect to critical origins — start DNS/TCP connections early for third-party domains
- Delay non-essential scripts — load chat widgets and popups after user interaction
- Self-host fonts — eliminate Google Fonts as a third-party render-blocking dependency
- Split JavaScript bundles — for sites with custom build processes
Combined with preloading your LCP image, fixing server response time, and resolving Core Web Vitals across all three metrics, eliminating render-blocking resources is the final piece of the LCP fix stack for most sites.
