Critical CSS and render-blocking resources explained
CSS and JavaScript can stop a page from showing. What render-blocking means, how critical CSS, defer and async fix it, and the trade-offs of each.
In this guide
What render-blocking means
CSS is render-blocking by design. Rules later in a stylesheet can override earlier ones, so the browser waits until it has received and processed all the CSS before it draws anything, instead of drawing unstyled content and then redrawing. Every stylesheet linked in the head therefore holds back the first paint until it has been downloaded and read.
Scripts work differently, and the difference matters. A script with no defer, async or type="module" blocks the HTML parser: the browser must download and run it before it reads the rest of the page. Strictly, it blocks parsing rather than rendering. In practice a script in the head still delays the first paint, because the browser cannot draw content it has not read yet.
When there are many blocking files, or they are large or slow to arrive, the visitor looks at a blank page. They hurt First Contentful Paint and, when the main element is text, LCP. Lighthouse 13 lists them under Render-blocking requests; older reports call the same thing "Eliminate render-blocking resources".
What Lighthouse counts
The insight lists requests that the browser marked as render-blocking and that finished before the first paint. A stylesheet in the head qualifies, and so does a synchronous script in the head. For scripts placed later in the body, only the ones with a high priority are listed. Each row shows the request and how long it took, and Lighthouse estimates the saving for requests that took 50 milliseconds or more. Its estimate treats the problem as one of the first paint first, and as an LCP problem when the LCP element is text rather than an image.
Fixing JavaScript: defer, async and modules
Add one of these to scripts that do not have to run before the page is drawn:
<script src="/js/app.js" defer></script>
<script src="/js/analytics.js" async></script>
<script src="/js/app.mjs" type="module"></script>- `defer` downloads the script while the page is being read and runs it after the document has been parsed, in the order the scripts appear. Use it for most scripts.
- `async` downloads the script while the page is being read and runs it as soon as it arrives, in any order, which can interrupt the parser. Use it for independent scripts such as analytics.
- `type="module"` scripts are deferred by default.
A script that must run first, such as one that decides which version of the page to show, can stay as it is. Keep those very small.
Fixing CSS: load less, and load it differently
CSS is harder, because the page really does need it. There are three ways to reduce the blocking.
1. Ship less CSS
Remove styles a page does not use. (The CSS performance guide covers the costs beyond loading.) Many themes and frameworks send one very large stylesheet to every page, and smaller files arrive sooner. The Coverage tool in Chrome DevTools shows how much of each file a page actually uses, and many performance plugins offer a remove unused CSS option.
2. Inline the critical CSS
Critical CSS is the small set of rules needed to draw the first screen. It goes inline in the head, so the browser has what it needs with no extra download. The full stylesheet is then loaded without blocking:
<style>
/* only what the first screen needs */
body { margin: 0; font-family: system-ui, sans-serif; }
.hero { min-height: 60vh; display: grid; place-items: center; }
</style>
<link rel="preload" href="/css/site.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/site.css"></noscript>The preload line starts downloading the full stylesheet without blocking the page, and the onload attribute turns it into a real stylesheet when it has arrived. The noscript line covers visitors without JavaScript. An inline onload handler is blocked by a strict Content-Security-Policy; if your site has one, let your build tool or performance plugin generate the equivalent code with a nonce or hash.
Why not load the CSS without blocking and stop there? The page would first be drawn with no styles and then redraw when the CSS arrives. That flash of unstyled content looks broken and can cause layout shifts. Critical CSS is what makes non-blocking CSS safe. On its own it does nothing for speed. It pays off together with a non-blocking load of the rest.
3. Split CSS by page or section
Load the styles for the checkout only on the checkout. With HTTP/2 and HTTP/3, several small files are fine, so one giant bundle is not a requirement. Avoid @import inside CSS files: each imported file is only discovered after the one that imports it has downloaded, which builds a chain.
Trade-offs
- Critical CSS has to match the page. If it was generated for one layout and the page differs, content can jump when the full stylesheet loads, which raises CLS.
- Critical CSS has to be regenerated when the design changes. Automated tools do this on every build or for each page.
- Keep inlined CSS small. A commonly quoted limit is about 14 KB, roughly what fits in the first round trip, and inlined CSS cannot be cached separately from the page.
- Mobile and desktop show different first screens. Good tools generate for both.
Fonts
Web fonts hold text back in a similar way. With the default setting, the browser may show invisible text until the font arrives. Use font-display: swap so text shows in a fallback font straight away, or optional to use the web font only if it is already there, and preload the one font the top of the page needs.
A sensible order of work
On a typical site, whether it is WordPress with a theme and a handful of plugins or a hand-built one, this order gets the most for the least effort:
- Add `defer` to scripts that nothing needs before the first paint. It is one word per script and rarely breaks anything. Test the menu, forms and sliders afterwards.
- Remove styles and scripts the page does not use. A plugin that loads its stylesheet on every page for a feature that exists on one page is a common find.
- Turn on or build critical CSS. Use your performance plugin's option, or a build step, and look at the page with a slow connection to confirm that nothing jumps.
- Handle fonts. Set
font-displayand preload the one font the top of the page needs. - Test again and compare. The list of render-blocking requests should be shorter, and First Contentful Paint should be earlier.
Stop when the list is empty or when the remaining files are tiny. A single small stylesheet that arrives in one round trip is not worth the complexity of splitting it.
How to check
PageSpeed Insights shows the render-blocking requests with their file names and durations. After a change, run the test again and check two things: the list is shorter, and the page still looks right while it loads. Watch the first second of a load on a slow connection, and check CLS, because a mismatch shows up as a jump.
Check your own page
Run the free audit and look for render-blocking requests. If the report lists a large stylesheet and a few scripts, start with the scripts, because adding defer is a one-word change, and then move on to the CSS.
Sources
- Critical rendering path on MDN: why CSS is render-blocking.
- The script element on MDN:
defer,async, module scripts and what blocks parsing and rendering. - font-display on MDN.
- Eliminate render-blocking resources in the Lighthouse documentation: the audit this insight replaced.
Start here
Free website audit
Live Google data for any public page: mobile and desktop.
Mobile + desktop · Lighthouse lab test · Real-user Chrome data when Google has it
Rather have this done for you? Speed optimization · Book a call