Render-blocking requests in Lighthouse: how to fix them
Lighthouse 13 lists every stylesheet and script your page waits for before it paints, the fast ones included. When that costs your visitors time, and which CSS fixes work.
Do render-blocking requests hurt my score?
Lighthouse lists every stylesheet and script your page waited for before its first paint in the Render-blocking requests insight ("Requests are blocking the page's initial render, which may delay LCP"), called "Eliminate render-blocking resources" before Lighthouse 13. The insight has no weight in your score, but the files on it delay FCP and LCP on mobile. Trim and combine your CSS, load the rest later and defer scripts.
Table of Contents!
What does the Render-blocking requests insight check?
In Lighthouse 13 and PageSpeed Insights the description ends: "Deferring or inlining can move these network requests out of the critical path." The table has three columns: URL, Transfer Size and Duration.
While Chrome loads a page, it marks every request as render-blocking or not and writes that into its trace. Lighthouse reads the trace and lists every request in the main frame that Chrome marked as blocking and that finished before the first paint. In practice that is the stylesheets and synchronous scripts in the <head> and the files they pull in with @import. A stylesheet in the body only counts at Chrome's highest priority, a script at high priority; after the first image both drop lower and are skipped. There is no threshold: one row fails the insight.
Some files never show up, even when they slow the page down: async scripts, files a script adds to the page (gtm.js, gtag.js), inline styles and scripts, anything in an iframe, and fonts.
Lighthouse 13 also fails pages the old audit passed. The old audit dropped every request under 50 ms and passed when nothing was left. Now such a request stays on the list with an empty Duration cell, and the insight turns orange. Files loaded through @import are on the list too: Lighthouse 11 and older read the HTML and never saw them.

The insight on a demo WooCommerce product page, Lighthouse 13.5.0, simulated throttling as in PageSpeed Insights.
Duration and "Est savings" are not measured. Lighthouse, and PageSpeed Insights with it, simulates them on a slow phone network (150 ms round trip, 1.6 Mbps) with the median server time of each domain. In my test a stylesheet that took 1.5 s on the server and one that took 20 ms got the same row: "Est savings of 140 ms".
What makes a file render-blocking?
Render-blocking means the browser paints nothing until that file is in: every stylesheet in the <head> has to arrive and every synchronous script there has to download and run. Often the wait is longer than the files themselves:
- A script after a stylesheet waits for that stylesheet. Chrome won't run a synchronous script until the CSS above it is in, and the parser waits for the script. Both files can download side by side and you still pay for the CSS and then for the script.
- An
@importstarts late. Chrome's preload scanner finds every link in the HTML at once, but an@importinside a CSS file only when that file has arrived. On the demo below, the imported theme stylesheet started at 1.28 s, against 0.59 s when it was linked from the HTML. - Another domain costs a connection. A stylesheet on fonts.googleapis.com or a CDN needs DNS, TCP and TLS before its first byte arrives.
| In Chrome | Holds the first paint? |
|---|---|
<link rel="stylesheet"> in the head | Yes |
the same with fetchpriority="low" | Yes |
the same with media="print" | No, still downloaded |
rel="preload" plus a stylesheet link to the same file | Yes |
@import in a blocking stylesheet | Yes, after its parent |
<script src> in the head | Yes |
async | No, but it can run before the paint |
defer, type="module" | No |
| a stylesheet in the body | Only the content below it |
Do render-blocking requests affect FCP and LCP?
I built a demo to measure it: a WooCommerce product page with the real WordPress, WooCommerce, Contact Form 7, Storefront, Font Awesome, jQuery and slick files, served over HTTP/2. Its head has nine stylesheets (one of them a fonts CSS on another domain), a tenth through @import and two synchronous scripts. Lighthouse lists twelve requests. The fixed version, with one trimmed stylesheet of 6.6 kB, the rest of the CSS loaded later and both scripts deferred, looks exactly the same once loaded. Chrome 155 emulating a phone, Slow 4G and 4x CPU slowdown, median of five runs.
FCP
The original demo painted first at 2.12 s, the fixed version at 1.39 s.

Before

After
Both versions at 1.4 s on the throttled phone: the original is still white.
LCP
LCP can't come before the first paint. On the demo the LCP element is the product photo, and it can't appear before the CSS is in either: LCP went from 2.25 s to 1.91 s with the fix. In an LCP breakdown that wait shows up as element render delay. Lighthouse doesn't count it: with an image as the LCP element it puts the LCP saving at 0 in every report. With a text LCP it copies the FCP saving.
How many sites are affected and is it safe to ignore?
Almost all of them. State of CWV checks the home pages of 114,526 sites with CrUX data: 92.8% have at least one render-blocking request, 91.2% a blocking stylesheet and 62.5% a blocking script. Where they have them, the median is 8 requests and 110 kB, and one site in five has more than 20. (State of CWV counts stylesheets and synchronous scripts in the head, a close match to Lighthouse's list.)
Sites with many of them do worse in the field. 84.0% of the sites without any have a good FCP on phones, against 54.1% of the sites with more than 20. But the server explains most of that drop: over the same range the median TTFB doubles, from 538 ms to 1,114 ms. Split by server speed:

Sites with a good FCP on phones by number of render-blocking requests and server response time. State of CWV, October 2026, CrUX p75 on phones, good FCP is 1.8 s or less.
- Fast server (TTFB under 600 ms): 94 to 97% have a good FCP, whatever the count. The time is still there: from first byte to first paint takes about 150 to 200 ms longer with 21 or more blocking requests than with none.
- Slow server (TTFB 900 ms or more): a good FCP drops from about half the sites (47 to 53%) with up to two blocking requests to 30% with 21 or more. A slow first byte leaves no room for the extra round trips.
On WordPress the counts run high: 57% of WordPress sites have 16 or more blocking requests (median 19, 163 kB). At the same server speed, that costs 3 points of good FCP against one to five requests on a fast host, and about 10 points on a slow one.
That is a correlation, but it sets the order: with a slow TTFB, fix the server first. On a fast server the 150 to 200 ms is still worth taking when it's your own CSS. You can leave the notice alone when:
- the only row left is the small stylesheet your first screen needs;
- the row is a consent tool, an A/B test snippet or a platform file you can't change (see below).
How do you see render-blocking requests in your field data?
Lighthouse loads your page once, cold. Your visitors come back with the CSS in their cache. In CoreDash look at two things on mobile:
- FCP next to TTFB. Render-blocking files cost their time between the first byte and the first paint. A template where that gap is larger than on the others loads more of them.
- LCP render delay. In State of CWV (sites with a TTFB under 900 ms) the median render delay grows from 192 ms on sites without blocking requests to 261 ms on sites with 16 or more.
CrUX has the same metrics at p75 for your origin or a URL, without telling you which template is slow.

CoreDash on a demo site, acme-pagespeed.com, mobile, p75: the product and category pages are the slow ones.
How do you find the blocking files in DevTools?
Open the page in Chrome, go to the Performance panel, set the network to Slow 4G and the CPU to 4x slowdown, and record a reload. The Insights sidebar lists Render-blocking requests. Click it and DevTools outlines each blocking request in the Network track.

Here the Duration is measured on your own load, so the slow file is the slow one. Look for the request that ends last before the FCP marker: the paint waited for that one. A CSS request that only starts when another one ends is an @import. When nothing blocks, the sidebar says "No render-blocking requests for this navigation".
How do you fix render-blocking requests?
The URL in the Lighthouse table tells you where each file comes from, and that decides where you change it.
Your own CSS and scripts
This is the head of the fixed demo, with shorter paths:
<link rel="stylesheet" href="/css/main.min.css">
<link rel="stylesheet" href="/css/rest.min.css" media="print" onload="this.media='all'">
<script src="/js/jquery.min.js" defer></script>
<script src="/js/gallery.min.js" defer></script> The first file is the nine stylesheets combined, with the rules the page doesn't use removed, the fonts hosted on the same domain and no @import. How each step works is further down.
WordPress
Files under /wp-content/plugins/ belong to a plugin, /wp-content/themes/ to your theme and /wp-includes/ to WordPress itself.
If you don't write code, an optimisation plugin can defer the scripts: Load JavaScript deferred in WP Rocket, Defer JavaScript in Perfmatters, Load JS Deferred in LiteSpeed Cache. Leave the Load CSS Asynchronously and critical CSS options off, whatever the old WordPress tip under this insight says about plugins that "inline critical assets". Critical CSS works, but the rest of the CSS arrives after the first paint, so the page styles itself twice and often shifts, and the inline part is downloaded again with every page. The same plugins offer Remove Unused CSS (Generate UCSS in LiteSpeed Cache), and I leave that off as well: it rebuilds your CSS for every page, often inline in the HTML, and brings as much trouble as critical CSS. Trim and combine the stylesheets yourself: it takes more work, and it pays for itself.
Many plugins load their CSS on every page, including pages where they do nothing: Contact Form 7 on pages without a form, WooCommerce outside the shop. Every stylesheet WordPress prints has an id such as contact-form-7-css: the part before -css is the handle you use to remove it where it isn't used:
add_action( 'wp_enqueue_scripts', function () {
if ( ! is_page( 'contact' ) ) {
wp_dequeue_style( 'contact-form-7' );
}
}, 20 ); WooCommerce has its own filter for its three stylesheets, woocommerce_enqueue_styles. For your own scripts, WordPress 6.3 and later can add defer:
wp_enqueue_script( 'acme-gallery', get_stylesheet_directory_uri() . '/js/gallery.min.js', array( 'jquery' ), '2.3.0', array( 'strategy' => 'defer' ) ); WordPress drops that defer again, without a warning, when a script that depends on yours is still blocking, or when an inline script is added after it. Check the script tag in the page source.
jQuery is the hard one. WordPress registers it without defer, so it blocks the head as soon as anything depends on it, and deferring it breaks every inline jQuery(...) call that runs before it. WP Rocket's Load JavaScript deferred defers jQuery and wraps those inline calls. Perfmatters' Defer JavaScript leaves jQuery out until you switch on Include jQuery.
A child theme whose style.css starts with @import url("../storefront/style.css") builds an @import chain. Remove that line and load the parent stylesheet with wp_enqueue_style.
Shopify
Apps add their files through app embeds (Online Store, Edit theme, App embeds). An embed's script file loads async (unless its Liquid writes its own script tag), its stylesheet blocks. Switch off the embeds you don't use, and after uninstalling an app, search theme.liquid and the snippets for the code it left behind.
Themes load their CSS with the stylesheet_tag filter in theme.liquid, which writes a blocking link. For the one base stylesheet that is fine. The script_tag filter writes a script without defer, so write that tag yourself:
<!-- blocks the first paint -->
{{ 'gallery.js' | asset_url | script_tag }}
<!-- doesn't -->
<script src="{{ 'gallery.js' | asset_url }}" defer></script> Shopify already sends 103 Early Hints for up to 10 render-blocking files in the head, so on Shopify the fix comes down to fewer and smaller files. Expect the accelerated checkout stylesheet to stay on the list (see below).
Next.js
In the App Router every CSS import becomes a stylesheet link at the top of the head, and React doesn't paint until it has loaded. Next.js merges them into a few chunks, so the fix is less CSS: global CSS imported in the root layout goes to every route, CSS Modules only to the routes that use them. More on how Next.js builds its CSS files.
Next.js loads its own scripts async or deferred. A blocking script on a Next.js site is one somebody added to the layout by hand. Load it with next/script, which by default loads it only once Next.js has started hydrating the page:
import Script from 'next/script'
<Script src="https://example.com/widget.js" strategy="afterInteractive" /> Google Fonts and other domains
The Google Fonts link is a blocking stylesheet on fonts.googleapis.com, and the font files come from a second domain, fonts.gstatic.com. On the demo that 0.5 kB file got the longest Duration in the table: Lighthouse counts the new connection. Host the fonts yourself and put the @font-face rules in your own stylesheet, and the request and both connections are gone. On Next.js, next/font already does this.
Consent tools, A/B tests and other files to leave alone
- A consent tool with auto-blocking (Cookiebot, OneTrust's OtAutoBlock, the Usercentrics autoblocker): it has to run before every other script to stop them, so it is synchronous by design. The alternative is manual mode, where you mark up every cookie script yourself. Whether that is worth the work is a legal question first. A preconnect to the tool's domain at least starts the connection early.
- An A/B testing snippet (Optimizely, AB Tasty, Convert): synchronous on purpose, so visitors don't see the original flash before the variant. Remove it when no test is running.
- Shopify's content_for_header: every theme needs it and you can't edit it. On all five stores checked for this page it added a blocking stylesheet for accelerated checkout, accelerated-checkout-backwards-compat.css. Leave it.
Trim and combine the CSS
On the median site 89% of the CSS is never used on the page (State of CWV, Chrome's coverage of the home page). The Coverage tab in DevTools shows it per file. Every kB of blocking CSS you remove is first paint you win back: on the demo about 20 ms per kB on the throttled phone.
Then combine the small files, and keep each render-blocking stylesheet under about 15 kB compressed. That is roughly what a new connection can deliver in its first round trip. It's a budget to aim for: a few kB over doesn't break anything. Don't merge everything into one big file. On the demo all the CSS, untrimmed, in one 68 kB file painted later than the original nine files.

Slow 4G in DevTools: every stylesheet waits about 0.6 s for its first byte. The four 0.3 kB files are nothing but that wait; the 14 kB file then downloads for 0.4 s, the 60 kB file for a full second.
Inlining all of it moves the bytes into the HTML, where the browser can't cache them for the next page. With a TTFB under 900 ms and one to five blocking requests, 95.9% of sites without inline CSS have a good FCP, against 90.8% of sites with more than 300 kB inline (State of CWV).
Replace every @import
Give each imported file its own link in the HTML, or combine it into the parent. On the demo removing the one @import moved the first paint 137 ms forward once the scripts were deferred. With the scripts still blocking it saved 33 ms, because jQuery finished at the same moment and hid the chain.
Load the rest of the CSS with media="print"
A stylesheet whose media doesn't match isn't render-blocking. Load the CSS for what sits below the first screen as print, and switch it to all when it has arrived:
<link rel="stylesheet" href="/css/rest.min.css" media="print" onload="this.media='all'"> On the demo that took 4.9 kB of CSS off the list. Restyling the page when it arrived took 32 ms, and nothing moved. The trap is the split: put CSS the first screen needs in that file, and the page paints unstyled first: Times, blue links, a photo wider than the screen. On the demo with all its CSS loaded this way, Lighthouse passed the insight with a 99, and CLS didn't count the restyle. You only see it on the screen, so check the split on a phone at Slow 4G before you trust the score.
Defer the scripts
Add defer to every script in the head that the first paint doesn't need. Module scripts are deferred already. Async takes a script off the list as well, but it can still run before the first paint and hold the main thread, so defer is the better default. Scripts are the smaller part of this insight today: on the demo deferring both scripts gained 24 ms, because the CSS finished later anyway. More in async vs defer, head vs footer JavaScript and 14 methods to defer JavaScript.
Start the main stylesheet early with 103 Early Hints
With 103 Early Hints the server tells the browser which files to fetch while it is still building the HTML. Hint the stylesheet the first screen needs:
HTTP/2 103
link: </css/main.min.css>; rel=preload; as=style On the demo, with 300 ms of server time, the first paint moved from 1.39 s to 0.65 s (DevTools throttling doesn't slow the HTML's headers, which flatters this number), while the file stayed on Lighthouse's list with 0.0 KiB. Check LCP afterwards: with the CSS in early, the 156 kB Font Awesome font started early too, took bandwidth from the product photo, and LCP went from 1.91 s to 2.60 s. The gain is at most your server's think time, so a page served from a CDN cache wins little. Cloudflare has a switch for it (Speed, Settings, Content Optimization) and builds the hints from the Link headers your server sends, and Apache 2.4.58 and later have H2EarlyHint. More on 103 Early Hints, and on why a plain preload of a stylesheet you also link doesn't help.
Skip fetchpriority="low" on a stylesheet
A common tip is to give the CSS the first screen doesn't need fetchpriority="low". It's an anti-pattern. In the head, Chrome lowers that stylesheet one step, from highest to high priority, and still waits for it before the first paint. Lighthouse decides what blocks by priority in its simulation, so it treats the file as if it were gone: no Duration, and a faster simulated FCP.
On the demo I set it on four stylesheets. Lighthouse with simulated throttling dropped FCP from 2.03 s to 1.60 s and raised the score from 96 to 97. In Chrome the first paint moved from 2,122 ms to 2,114 ms, so your visitors wait as long as before.

Lighthouse, simulated throttling

Chrome on the phone, same page
At 1.6 s, the FCP Lighthouse reports, Chrome still shows a white screen.
How do you check that the fix worked?
Run Lighthouse on mobile again. On the demo the score went from 96 to 100 and FCP from 2.0 s to 1.1 s, and the insight went from twelve red rows to one orange row: the 6.6 kB stylesheet. A page that needs CSS for its first screen keeps that row.

Original demo

Fixed demo
Lighthouse 13.5.0 on the demo, mobile, simulated throttling.
Run it three times: when the demo was served very fast, its simulated FCP swung by 1.5 s between runs for reasons that had nothing to do with the CSS.
CoreDash shows FCP and LCP render delay from the next visits on. CrUX and Search Console show the last 28 days, so two weeks after the fix half of what they show is still the old site.
Want to know whether blocking CSS is what keeps your mobile FCP or LCP red? In a Core Web Vitals power hour we open your field data and a trace of your page together, and you leave with the files to trim, combine or load later.
I write the code, not the report.
I join your team for 1 to 2 sprints. I set up the monitoring and make sure your team keeps the metrics green after I leave.
Get in touch