Skip to main content
All fixes

Page speed, redirects, Core Web Vitals

Eliminate render-blocking resources slowing first paint

Render-blocking CSS and JavaScript delay First Contentful Paint by hundreds of milliseconds. Inline critical styles and defer the rest to unblock the browser.

What's happening

Render-blocking resources are external CSS and synchronous JavaScript referenced in the document head that the browser must download and parse before it can render any content. The Lighthouse "Eliminate render-blocking resources" opportunity quantifies the savings, often 500-2000ms on 4G mobile. The browser blocks rendering on these because executing them might add elements to the DOM or change the visual presentation of what's already there.

CSS is render-blocking by default — the browser won't paint until the CSSOM is constructed. JavaScript is parser-blocking unless marked async or defer. The Chrome DevTools Network panel shows blocking resources with a red "Blocking" indicator in the Initiator column, and the Performance panel highlights them in the Network track.

The question isn't whether to load CSS and JS — it's whether they need to load before first paint. Critical CSS for above-the-fold content is unavoidably blocking; everything else can be deferred.

Why it matters

Render-blocking resources directly delay FCP, which delays LCP, which fails Core Web Vitals at the 75th percentile. The cascade hurts mobile rankings most because mobile parse and download times are 3-5x slower than desktop.

User-facing, render-blocking resources show up as a long blank-page period after the initial HTML arrives. The browser has the markup but can't show it because it's still waiting for stylesheets. On 4G mobile this period can stretch to 2-3 seconds for sites with heavy CSS frameworks loaded synchronously.

Common causes

  • External tags in for the entire site CSS bundle.
  • Synchronous tags without defer or async attributes.
  • Third-party widget scripts (chat, analytics, A/B testing) loaded in the head.
  • Web fonts referenced via @import in CSS (which compounds blocking).
  • Server-rendered CSS-in-JS extracting all styles into one giant blocking.
  • without the swap-to-stylesheet pattern.

Detect this on your site

Run a quick scan with the Speed Test. The tool surfaces this exact issue with the records and context needed to apply the fix below.

Open Speed Test

How to fix it

  1. 1

    List blocking resources via Lighthouse

    Run Lighthouse and open the "Eliminate render-blocking resources" audit. It shows every blocking URL with download size and potential savings. Tackle the largest ones first — a 50KB blocking CSS file costs more than ten 5KB files.

  2. 2

    Extract and inline critical CSS

    Use Critical, beasties, or Penthouse to extract above-the-fold styles and inline them in in. The full stylesheet then loads non-blocking via the preload-then-stylesheet pattern. Next.js's experimental optimizeCss flag does this automatically.

  3. 3

    Add defer or async to every script

    Audit every tag. Use defer for scripts that depend on the DOM and need to run in order (analytics, app code). Use async for scripts that don't depend on order or DOM (counters, beacons). The Next.js Script component handles this with strategy="afterInteractive" or "lazyOnload".

  4. 4

    Move third-party scripts off the critical path

    Chat widgets, A/B testing, heatmaps, and customer-data platforms don't need to load before first paint. Use Partytown to run them in a Web Worker, or load them on user interaction (idle callback, scroll, click).

  5. 5

    Split CSS by route

    Don't ship the entire site's CSS on every page. Tailwind v4 with CSS layers and Next.js's per-route CSS extraction limit the blocking CSS to what's actually used on this page. Audit your largest CSS files in the Coverage tab — anything below 50% used is split candidate.

  6. 6

    Avoid @import in CSS

    @import inside a stylesheet creates a sequential dependency: the browser has to parse the parent CSS, find the @import, and only then start downloading the imported file. Replace with multiple tags in HTML, which load in parallel.

Example

<!-- Bad: blocks render until full.css downloads and parses -->
<link rel="stylesheet" href="/full.css">

<!-- Good: inline critical, async-load the rest -->
<style>/* critical CSS extracted by Critical/beasties */</style>
<link rel="preload" href="/full.css" as="style"
      onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/full.css"></noscript>

<!-- Bad: parser-blocking JS -->
<script src="/app.js"></script>

<!-- Good: defer DOM-dependent JS, async independent JS -->
<script defer src="/app.js"></script>
<script async src="/analytics.js"></script>

Critical inline, rest deferred via preload-then-stylesheet pattern.

Frequently asked

Defer downloads the script in parallel with HTML parsing and runs it after parsing completes in document order. It doesn't block FCP though it can delay TTI if the script does heavy work on execution.

No. Inlining everything bloats the HTML blows past TCP's initial congestion window and prevents browser caching. Inline only the critical above-the-fold styles (typically 14KB or less) and load the rest async.

Async. Analytics doesn't depend on the DOM and order doesn't matter. Async lets the browser fire it whenever the network is idle with minimum impact on the critical path.

Related fixes