How to eliminate render-blocking CSS and JavaScript

Browser loading timeline in which stylesheet and script bars hold back the first paint until they are deferred

A render-blocking resource is a file the browser must download and process before it can paint anything on screen. By default, that means every stylesheet in the <head> and every script without defer, async or type="module". To eliminate render-blocking resources, inline the small amount of CSS the first screen needs, load the rest of your CSS without blocking, defer your scripts, and delay third-party tags until the page is usable.

Each blocking file costs at least one network round trip before first paint, and more when it lives on another domain or pulls in further files. Near your server that is barely visible. From another continent, where a round trip typically takes 150–300 ms, a few blocking requests can add a second or more to First Contentful Paint and Largest Contentful Paint.

How the critical rendering path decides first paint

The critical rendering path is the sequence of steps between receiving HTML and drawing pixels: parse the HTML into the DOM, parse CSS into the CSSOM, combine them into a render tree, calculate layout, then paint. The browser does not paint until it has built the CSSOM for every stylesheet that applies to the page, because painting unstyled content first would flash a broken page.

JavaScript makes the path longer. A classic <script src> without attributes stops the HTML parser: the browser downloads the file, runs it, and only then continues parsing, because the script could rewrite the document. A script placed after a stylesheet also waits for that stylesheet, since it might read computed styles.

The browser's preload scanner reads ahead in the raw HTML and starts downloads early, but it cannot fix chains, where one file reveals the next only after it arrives. Aim for a short critical path: few, small blocking files on origins the browser is already connected to. Google's article on the critical rendering path covers the full model.

What counts as a render-blocking resource

Stylesheets in the head

Every <link rel="stylesheet"> without a media attribute, or with one that matches the current device, blocks rendering. A 300 KB framework stylesheet blocks first paint even when the first screen uses a small fraction of it.

Synchronous scripts

Scripts in the head without defer, async or type="module" block parsing. Typical offenders: jQuery plugins, A/B testing snippets, consent banners and analytics code pasted at the top of the template.

CSS imports and request chains

An @import inside a stylesheet creates the worst kind of chain. The browser discovers theme.css only after it has downloaded and parsed main.css, so every level adds another full round trip. Bundle imported files at build time, or at least use separate <link> tags.

Web fonts

Font files block text rather than the whole page: by default, most browsers hide text in a loading web font for up to about 3 seconds. A standard Google Fonts embed also adds a render-blocking stylesheet on fonts.googleapis.com that pulls font files from fonts.gstatic.com: two extra origins, each with its own DNS, TCP and TLS setup.

Defer vs async vs module scripts: which to use

Three attributes take a script off the critical path. They differ in when it runs and whether order is kept.

Script typeDownloadRunsOrder keptBlocks parsingBest for
No attributeParser waits for itImmediatelyYesYesAlmost nothing in the head
asyncIn parallelAs soon as it arrivesNoOnly while it runsIndependent tags: analytics, ads
deferIn parallelAfter HTML is parsed, before DOMContentLoadedYesNoYour own code, anything touching the DOM
type="module"In parallel, plus its importsLike defer (or like async if you add it)YesNoModern bundles and ES modules
<!-- Blocks the parser: avoid -->
<script src="/js/app.js"></script>

<!-- Runs in order after parsing -->
<script src="/js/vendor.js" defer></script>
<script src="/js/app.js" defer></script>

<!-- Independent: runs on arrival -->
<script src="https://analytics.example.com/tag.js" async></script>

<!-- Module: deferred by default -->
<script type="module" src="/js/main.mjs"></script>

Keep deferred scripts in the head so the download starts early and overlaps with parsing. defer has no effect on inline scripts, and deferred scripts still run before the load event, so they still count toward full load time.

Don't add async blindly. If app.js depends on vendor.js, making both async lets them run in either order and breaks the page intermittently. Use defer for dependent scripts.

Inline critical CSS and load the rest without blocking

Inline the CSS for the first screen

Critical CSS is the minimum set of rules needed to render the first viewport: base typography, the header, the layout grid and the hero. Inline it in a <style> block so it arrives with the HTML. Keep it small: if the HTML plus inline CSS fits within roughly 14 KB compressed, it usually arrives in the server's first flight on a new connection, because servers typically start with an initial congestion window of 10 packets. Tools such as Critical and Penthouse extract these rules automatically.

<head>
  <style>
    /* Critical rules only */
    body { margin: 0; font: 16px/1.5 system-ui, sans-serif; }
    .hero { min-height: 60vh; display: grid; place-items: center; }
  </style>
  <!-- Full stylesheet, non-blocking -->
  <link rel="stylesheet" href="/css/site.css" media="print" onload="this.media='all'">
  <noscript><link rel="stylesheet" href="/css/site.css"></noscript>
</head>

The media="print" trick works because browsers fetch stylesheets for non-matching media at low priority without blocking rendering; the onload handler then applies the file to all media. If your Content Security Policy forbids inline handlers, attach the listener from an external script. web.dev explains the pattern in its guide to deferring non-critical CSS.

Use media attributes for conditional stylesheets

Print-only and wide-screen stylesheets need only an accurate media attribute, such as media="print" or media="(min-width: 1024px)", to stop blocking first paint where they don't apply.

Remove unused CSS and split JavaScript

  • Open the Coverage panel in Chrome DevTools and reload to see how much of each CSS and JS file goes unused (often more than half for themes), then drop unused selectors with PurgeCSS or your framework's content scanning.
  • Split JavaScript by route with dynamic import(), and load carousels, maps and date pickers on scroll or first interaction.
  • Stop loading plugin assets site-wide: a contact form's CSS and JS belong on the contact page only.

Preload, preconnect, dns-prefetch and modulepreload

Resource hints don't remove blocking files, but they shorten chains by starting work early.

  • preconnect opens the DNS, TCP and TLS connection to another origin early. Use it for the one to three origins your first screen needs; unused connections are dropped after a few seconds.
  • dns-prefetch resolves only the hostname, which is cheap enough for less critical origins such as a tag manager.
  • preload fetches one specific file at high priority right away. It suits late-discovered critical files: a font referenced from CSS or a hero image set as a CSS background.
  • modulepreload fetches, parses and compiles an ES module ahead of time, so it is ready when the script runs.
<link rel="preconnect" href="https://img.example-cdn.com">
<link rel="dns-prefetch" href="https://www.googletagmanager.com">
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
<link rel="modulepreload" href="/js/main.mjs">

When preload hurts

  • Too many preloads. Every preload competes for bandwidth with the HTML, critical CSS and the LCP image. Preloading ten files is close to having no priorities at all.
  • Files the page doesn't need yet. Chrome logs a console warning when a preloaded file goes unused for a few seconds after load: bandwidth spent for nothing.
  • Mismatched attributes. A font preload without crossorigin, even on your own domain, or a wrong as value makes the browser download the file twice.
  • An LCP image already in the HTML. Give the <img> fetchpriority="high" instead of preloading it.

Fonts and third-party scripts

Self-host fonts and use font-display swap

Self-hosting WOFF2 files on your own domain removes the two extra connections a hosted font service needs. The old shared-cache argument is gone too: major browsers partition their HTTP caches by site, so a font cached on another website downloads again on yours. Then show fallback text while it loads:

@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-var.woff2") format("woff2");
  font-weight: 100 900;
  font-display: swap;
}

swap renders text immediately in a fallback font and switches when the web font arrives. optional uses the web font only if it arrives within roughly 100 ms, which avoids late reflow and protects CLS. Subset fonts to the characters you need, stick to one or two families, prefer one variable font over many static weights, and tune fallback metrics with size-adjust. See web.dev's font best practices for details.

Facades and delayed loading for third-party scripts

Third-party code often adds the most requests and main-thread work. Work through it in order:

  1. Prune your tag manager. Delete old pixels, heatmaps and abandoned experiments, and fire the remaining non-essential tags on Window Loaded instead of the initial page view.
  2. Replace anti-flicker snippets. Some A/B testing tools hide the whole page until their script loads or a timeout passes, which makes them render-blocking in practice. Prefer server-side experiments.
  3. Use facades for heavy widgets. Show a lightweight placeholder, such as a styled chat button or a video thumbnail, and load the real widget when the visitor clicks it.
<button type="button" id="chat-button">Chat with us</button>
<script>
  function loadChat() {
    var s = document.createElement('script');
    s.src = 'https://widget.example-chat.com/loader.js';
    s.async = true;
    document.head.appendChild(s);
  }
  // Load on first click...
  document.getElementById('chat-button')
    .addEventListener('click', loadChat, { once: true });
  // ...or 3 seconds after the load event:
  // addEventListener('load', function () { setTimeout(loadChat, 3000); });
</script>

Minify and compress text assets with Brotli

Once the number of blocking files is down, make the rest small. Minify CSS, JavaScript and HTML in your build with esbuild, Terser, Lightning CSS or cssnano, then compress every text response. Brotli typically produces files 15–20% smaller than gzip for CSS and JavaScript, and every current browser accepts it over HTTPS. On nginx with the ngx_brotli module:

brotli on;
brotli_comp_level 5;
brotli_static on;   # serve pre-compressed .br files when they exist
brotli_types text/css application/javascript application/json
             image/svg+xml text/plain;

On Apache, use mod_brotli: AddOutputFilterByType BROTLI_COMPRESS text/html text/css application/javascript. Cloudflare and most CDNs apply Brotli automatically. Verify it:

curl -sI -H "Accept-Encoding: br" https://www.example.com/css/site.css | grep -i content-encoding
# content-encoding: br

Compression reduces bytes, not round trips. On a warm connection, a 10 KB stylesheet and a 40 KB one cost about the same single round trip, so removing or inlining blocking files usually matters more for distant visitors than shaving kilobytes. Serving them from a CDN also shortens each round trip, as our CDN guide explains.

Measure render-blocking fixes with Global Website Speed Test

Run a free 8-region speed test before you change anything, then again after each fix.

File count and load time per region

The Files column shows how many requests each region made on the first visit, and the report adds a "Too many files" tip above 80. In each region's timeline, the DNS, TCP, TLS and server wait segments end at the first byte of HTML. The page load segment after it is where blocking files, scripts and images spend their time.

Origin versus distant regions

The origin badge marks the region with the fastest TTFB, usually the one closest to your server. Each sequential blocking request costs at least one round trip, and from far regions a round trip can be ten to fifty times longer. A chain of main.css, an imported theme.css and a web font costs three round trips after the HTML arrives: typically under 50 ms near a Virginia server, but around 540 ms from Seoul at a 180 ms round-trip time. A rough diagnostic: compute load time minus TTFB for the origin and for your slowest region, then divide the gap between them by the gap in TTFB. The result approximates how many round trips your page needs in sequence. Above 10, shorten your chains first.

First visit versus repeat visit

On the repeat visit, CSS and JavaScript should come from the browser cache. If repeat load time drops sharply in far regions, network round trips were the main cost; give static files long cache lifetimes, as our browser caching guide describes. If the repeat visit is barely faster, the time goes to script execution or uncacheable third-party tags.

Find blocking files in Chrome. Paste performance.getEntriesByType('resource').filter(e => e.renderBlockingStatus === 'blocking').map(e => e.name) into the DevTools console to list every file that blocked rendering.

Deferring alone may not move full load time much, even when first paint gets faster. Track paint with LCP in field data, as our Core Web Vitals guide explains: good is 2.5 seconds or less at the 75th percentile. Removing tags and delaying third parties until after the load event improves both. Our testing methodology describes what each region measures.

Frequently asked questions

What are render-blocking resources?

Render-blocking resources are files the browser must download and process before it can paint the page. By default these are stylesheets in the head and scripts without the defer, async or type=module attribute. Each one adds at least one network round trip before first paint.

Should I use defer or async for JavaScript?

Use defer for your own scripts and for anything that touches the DOM or depends on another script, because deferred scripts run in document order after the HTML is parsed. Use async only for independent scripts, such as analytics, that can run in any order.

Does inlining critical CSS really help?

Yes, as long as the inlined CSS stays small. Inlining the styles for the first screen removes a blocking request, so the browser can paint as soon as the HTML arrives. Keep the HTML plus inline CSS near 14 KB compressed and load the full stylesheet without blocking.

Is preloading always good for performance?

No. Preload raises a file's priority, so preloading many files, or files the page does not need right away, takes bandwidth from truly critical resources. Limit it to one to three late-discovered critical files, such as the main web font, and add the crossorigin attribute for fonts.

Why do render-blocking files hurt more in other countries?

Each blocking file needs at least one round trip to the server before the page can paint. A round trip from Seoul to a server in Virginia typically takes around 180 ms, compared with a few milliseconds nearby, so a chain of three or four blocking requests can add more than half a second for distant visitors.

DevTeam Engineering builds and runs Global Website Speed Test, a free tool that measures page speed from 8 AWS regions at once. We publish guides based on what we see in thousands of multi-region tests. How we test.

Related guides

Three gauge dials labeled LCP, INP and CLS with their needles in the green zone, above a browser window and a world map
Core Web Vitals

Core Web Vitals explained: how to improve LCP, INP and CLS

Core Web Vitals explained: what LCP, INP and CLS measure, the good thresholds at the 75th percentile, and the practical fixes that move each metric to green.

9 min read · Updated Oct 4, 2026
A browser window pulling CSS, JavaScript, font and image files from a local cache drawer instead of a distant server, with a stopwatch showing a faster second visit
Caching

Cache-Control and browser caching: how to make repeat visits instant

Browser caching: set Cache-Control headers by file type, use immutable and stale-while-revalidate, bust caches with content hashes and speed up repeat visits.

9 min read · Updated Oct 4, 2026
Timeline comparing the round trips TCP with TLS 1.2, TCP with TLS 1.3 and QUIC need before the first byte arrives
Network & Protocols

HTTP/2, HTTP/3 (QUIC) and TLS 1.3: protocol-level speedups explained

HTTP/3 over QUIC and TLS 1.3 cut connection setup from three or four round trips to one or two. Learn how they work, how to enable them and how to verify.

9 min read · Updated Oct 4, 2026