Skip to main content

How to Fix Render-Blocking Resources

8 min readBy SEO Snapshot

What "Render-Blocking" Actually Means

When the browser parses your HTML and hits a <link rel="stylesheet"> or a plain <script src>, it can't just keep going. Those resources sit on the critical rendering path — the sequence of work the browser must finish before it can paint a single pixel.

CSS blocks rendering because the browser refuses to paint content it might have to restyle. It builds the DOM from HTML and the CSSOM from your stylesheets, then combines them into the render tree. Until the CSSOM is complete, there's no render tree, so there's nothing to show. A stylesheet in the <head> therefore holds back First Contentful Paint (FCP) for as long as it takes to download and parse.

Synchronous JavaScript blocks for a different reason: it can call document.write() or mutate the DOM, so the parser stops dead at each <script> tag, fetches the file, executes it, and only then resumes parsing. Worse, if a stylesheet is still loading when the parser reaches a script, the browser also waits for that CSS first (scripts might read computed styles). One slow .js file in the head can stall everything behind it.

That's the whole problem in one sentence: resources in the head delay the first paint, and delaying the first paint delays LCP and hurts your Core Web Vitals.

A timeline comparing three script-loading modes and when each lets the page paint: a plain blocking script pauses HTML parsing while it downloads and executes so first paint is latest, an async script downloads in parallel but its execution can still interrupt parsing so paint timing is unpredictable, and a defer script downloads in parallel and runs only after parsing finishes so first paint is earliest and never blocked.
Blocking vs async vs defer: how each attribute shifts when the browser can paint.

Reading the Lighthouse Audit

In PageSpeed Insights and Lighthouse the audit is called "Eliminate render-blocking resources." It lists each blocking URL with two numbers: Transfer size and Potential savings (ms). The savings estimate is Lighthouse's guess at how much FCP would improve if that resource stopped blocking — it's a lab estimate, not a promise, but it tells you where to spend effort.

Sort by savings and work top-down. A 4 ms saving on a tiny stylesheet isn't worth touching; a 900 ms blocking third-party script is. SEO Snapshot's analyzer also lists render-blocking scripts and stylesheets with their URLs when you run your page, which is a fast way to see what's in the head before you open DevTools.

defer vs async vs module

For scripts, the fix is almost always an attribute change. Here's how the three loading modes actually behave:

Attribute Downloads Blocks parser? Executes Order preserved Use for
(none) immediately Yes as soon as fetched yes almost never in <head>
async in parallel no the moment it arrives no independent scripts: analytics, ads
defer in parallel no after HTML parsing, before DOMContentLoaded yes app code, anything with dependencies
type="module" in parallel no deferred by default, after parsing yes (per graph) modern ES modules
<!-- Your main app bundle: order matters, wait for the DOM -->
<script src="/js/app.js" defer></script>

<!-- Fire-and-forget, no dependencies -->
<script src="https://analytics.example.com/tag.js" async></script>

<!-- ES modules are deferred automatically -->
<script type="module" src="/js/main.mjs"></script>

The mental model: reach for defer by default. Use async only when the script genuinely doesn't care about DOM readiness or execution order. Note that type="module" is deferred implicitly — adding defer to a module does nothing.

The Critical-CSS + Async-Load Pattern

CSS has no defer. The standard trick is to inline the small slice of CSS needed to render above-the-fold content, then load the full stylesheet without blocking:

<head>
  <style>/* critical, above-the-fold CSS only — header, hero, layout */</style>

  <link rel="stylesheet" href="/css/full.css"
        media="print" onload="this.media='all'">
  <noscript><link rel="stylesheet" href="/css/full.css"></noscript>
</head>

media="print" makes the browser treat the stylesheet as non-render-blocking (it's not for the screen), so it downloads without holding up paint. The onload handler flips media back to all once it arrives, applying the styles. The <noscript> fallback matters: with JavaScript disabled, onload never fires, so you'd ship an unstyled page without it.

The caveat nobody mentions: if your critical CSS is wrong or incomplete, the async stylesheet lands a moment later and restyles the page — a visible flash. Tools like critical or critters (used by some frameworks) extract above-the-fold CSS automatically. Get the critical set right or you're trading a slow paint for a janky one.

Splitting CSS by Media Query

You don't always need the async trick. The browser downloads stylesheets with non-matching media at a low priority and doesn't let them block render:

<link rel="stylesheet" href="/css/base.css">
<link rel="stylesheet" href="/css/print.css" media="print">
<link rel="stylesheet" href="/css/desktop.css" media="(min-width: 1024px)">

On a phone, desktop.css and print.css don't block the first paint. Splitting one giant styles.css into media-scoped files is a low-risk win if your CSS is already organized by breakpoint.

preload vs preconnect vs dns-prefetch

These three resource hints get confused constantly. They solve different problems:

<!-- Fetch a specific resource early, at high priority -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>

<!-- Open the full connection (DNS + TCP + TLS) to a known origin -->
<link rel="preconnect" href="https://cdn.example.com" crossorigin>

<!-- Just resolve DNS — cheap, for origins you'll touch but not immediately -->
<link rel="dns-prefetch" href="https://analytics.example.com">
  • preload tells the browser to fetch this exact file now because it's needed soon (fonts, the LCP image, a critical CSS chunk). It does not block render, but it competes for bandwidth.
  • preconnect warms up a connection to a third-party origin so the eventual request skips the handshake — worth ~100–300 ms on a fresh TLS connection. Use it for origins you're certain you'll hit.
  • dns-prefetch does only the DNS lookup. It's the lightweight fallback for preconnect on origins that are lower priority or many in number.

Third-Party Scripts

Third-party tags — tag managers, chat widgets, A/B testing, ads — are often the biggest offenders because you don't control their size or timing. Rules that hold up:

  • Load them async, never synchronously in the head.
  • preconnect to their origin so the request is ready.
  • Delay non-essential widgets (live chat, heatmaps) until user interaction or requestIdleCallback — they don't need to run during first paint.
  • Self-host what you can. A self-hosted analytics snippet avoids an extra DNS + TLS round trip.

Common Mistakes

Deferring a script an inline script depends on. If you defer jQuery but keep an inline <script> that calls $(...) in the body, the inline script runs first and throws $ is not defined. Deferred scripts run after parsing; inline scripts run immediately. Either defer both or convert the inline code to a DOMContentLoaded listener.

Preloading everything. preload is a priority instruction. Preload ten things and you've told the browser nothing is more important than anything else — you'll delay your real LCP resource. Preload the one or two assets on the critical path, no more. Chrome will warn in the console when a preloaded resource isn't used within a few seconds.

Inlining too much CSS. Inlined CSS isn't cached and ships on every HTML response. Inline your full 60 KB stylesheet and every page load re-downloads it, bloating the HTML and hurting repeat visits. Keep the inline block to genuine above-the-fold styles — usually a few KB.

Before / After

<!-- BEFORE: three blocking resources in the head -->
<head>
  <link rel="stylesheet" href="/css/styles.css">
  <script src="/js/jquery.js"></script>
  <script src="/js/app.js"></script>
</head>
<!-- AFTER: nothing blocks the first paint -->
<head>
  <style>/* critical CSS */</style>
  <link rel="stylesheet" href="/css/styles.css"
        media="print" onload="this.media='all'">
  <noscript><link rel="stylesheet" href="/css/styles.css"></noscript>
  <script src="/js/jquery.js" defer></script>
  <script src="/js/app.js" defer></script>
</head>

Measuring the Win

Don't trust the score alone — measure FCP and LCP before and after. Run the page in an incognito window with the DevTools Performance panel, or re-run Lighthouse in a controlled environment (throttling on, no extensions). Field data in the Chrome UX Report is the real judge, but it lags weeks behind, so use lab numbers to iterate. Fixing render-blocking on a page that had two blocking scripts and a full stylesheet in the head commonly moves FCP by 0.3–1 s and LCP by 0.5–2 s.

If you want the framework-specific version of all this, see fixing render-blocking resources in Next.js. For the broader picture, website speed optimization and getting a Lighthouse score of 100 cover the other audits that move alongside this one.

FAQ

Does defer slow anything down? No. Deferred scripts still download in parallel during parsing; they just execute after the DOM is built. For most sites defer is strictly better than a blocking script in the head.

Should I inline all my CSS to pass the audit? No. Inline only above-the-fold critical CSS and load the rest asynchronously. Inlining everything kills caching and bloats every HTML response.

Is async faster than defer? Not meaningfully — both download without blocking. async executes sooner but in unpredictable order, which breaks dependent code. Use defer unless the script is truly independent.

Why does Lighthouse still flag a stylesheet after I split it? Check the media attribute actually doesn't match the test conditions. Lighthouse runs mobile emulation by default, so a media="(min-width: 1024px)" sheet won't block — but a plain screen stylesheet still will.

Check your site's SEO score for free

Analyze your site