Critical CSS & Render-Blocking Resources: Painting Without Waiting
This topic is part of Rendering & CSS Performance. Before a browser paints anything, it needs the styles that apply to the page; otherwise it would paint unstyled content and then repaint it, a jarring flash. So browsers treat stylesheets in the document head as render-blocking: no first paint until they have downloaded and been parsed. Synchronous scripts in the head are also blocking: the parser stops until they have downloaded and executed, and they in turn wait for any preceding CSS.
On a fast connection with a small stylesheet from the same origin, this is barely noticeable. On a mobile network, with a 180KB framework stylesheet on a separate CSS domain plus a web font stylesheet from a third party, the browser can sit with HTML in hand and nothing on screen for a second or more. That time is pure waiting — the main thread is mostly idle — and it delays First Contentful Paint (FCP) and Largest Contentful Paint (LCP) for every visitor.
The fix has three parts: give the browser the small set of rules it needs for the first viewport immediately (critical CSS, inlined), load everything else without blocking, and ship less CSS overall by removing unused rules. Each part has trade-offs around caching, flashes of unstyled content and build complexity, which this topic and its guides work through.
The Metric Degradation This Topic Addresses
Render-blocking resources show up first in FCP: the gap between the HTML response arriving and anything appearing on screen. Because LCP cannot happen before the first paint, they also inflate LCP, often by the full duration of the CSS download. In the LCP breakdown, this appears as render delay (if the LCP resource has loaded but cannot be painted until CSS arrives) or as load delay (if the LCP image is discovered late because it is referenced from CSS).
Lighthouse reports these under "Eliminate render-blocking resources", listing each stylesheet and script with its estimated savings. In field data, a large gap between TTFB and FCP at p75 on mobile — more than about 1 second — is a strong signal that blocking resources are the bottleneck. A secondary effect is CLS: when CSS is loaded asynchronously without enough critical rules, the page renders partially styled and then shifts as the full stylesheet applies.
Prerequisites
- A list of every stylesheet and script in the document head of key templates, with size, origin and whether it is blocking.
- Lab and field FCP and LCP for those templates, segmented by device class.
- Access to the build pipeline, so CSS extraction and inlining can be automated rather than maintained by hand.
1. Environment Setup: Inventory Blocking Resources
Open DevTools, reload with mobile throttling, and in the Network panel enable the "Render blocking" column (or look at the renderBlockingStatus in Resource Timing). Every request marked blocking delays first paint. For each stylesheet, note size, cache policy, and whether it is on a separate origin (which adds DNS, TCP and TLS setup). For each head script, note whether it is async, defer, type="module", or synchronous.
// Paste in the console after load: list render-blocking resources.
performance.getEntriesByType('resource')
.filter((r) => r.renderBlockingStatus === 'blocking')
.map((r) => ({ url: r.name, kb: Math.round(r.transferSize / 1024), ms: Math.round(r.duration) }));
// trade-off: renderBlockingStatus is Chromium-only; use Lighthouse or WebPageTest
// for cross-browser checks.
2. Capture a Baseline
Record FCP and LCP in the lab (several runs, median) and in the field at p75. Record the CSS coverage: the Coverage tab in DevTools shows, per stylesheet, how many bytes were used on the current page. Coverage for a typical marketing page with a framework stylesheet is often 10–25% used.
3. Isolate Critical Rules
Critical CSS is the subset of rules needed to render the content visible in the initial viewport, across the viewport sizes you care about. Tools such as critical, critters (Beasties) and framework plugins extract it by rendering the page in a headless browser and collecting rules that match above-the-fold elements. The output should be small — under roughly 14KB compressed is a common target so it fits in the first round trips — and should cover layout, typography, header, hero and any above-the-fold components.
4. Apply: Inline, Defer, Shrink
Inline the critical CSS in a <style> element in the head. Load the full stylesheet asynchronously, so it applies after first paint without blocking. Remove unused rules from the full stylesheet with PurgeCSS or framework-level tree shaking. Make head scripts defer (or type="module", which is deferred by default) unless they truly must run before first paint.
<head>
<style>/* inlined critical CSS */</style>
<link rel="stylesheet" href="/css/site.4f2a1c.css" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="/css/site.4f2a1c.css"></noscript>
<script src="/js/app.91bd.js" defer></script>
</head>
<!-- trade-off: the media-swap trick depends on JavaScript for the swap; the
noscript fallback keeps the page styled without it. -->
Deconstructing the Critical Rendering Path
The critical rendering path is the sequence of steps from receiving HTML to painting: parse HTML into the DOM, fetch and parse CSS into the CSSOM, combine them into the render tree, lay out, and paint. Three properties of this path explain most render-blocking problems.
First, CSS blocks rendering, not parsing. The HTML parser keeps going while a stylesheet downloads, and the preload scanner discovers later resources; the browser simply will not paint. Second, CSS blocks script execution. A synchronous script after a stylesheet must wait for that stylesheet, because the script might query computed styles. This is why a slow stylesheet can also delay JavaScript that appears to be unrelated. Third, only matching media is blocking. A stylesheet with media="print" or media="(min-width: 1200px)" on a phone does not block rendering; it is still downloaded at low priority. This is the basis of the media-swap pattern for async CSS.
Fonts add another layer: a web font stylesheet from a third-party origin is render-blocking CSS, and the font files it references are discovered only after it is parsed. Self-hosting fonts, preloading the main font file and using font-display: swap or optional removes that chain from the critical path.
Advanced Diagnostics and Edge Cases
CSS imported with @import. Each @import is discovered only after the parent stylesheet downloads, creating a serial chain. Flatten imports at build time.
CSS on a separate origin. A CDN subdomain for CSS requires a new connection. Serve critical-path CSS from the same origin as the HTML, or at least preconnect early.
HTTP/2 push and Early Hints. Push is deprecated; 103 Early Hints can start the stylesheet download while the server is still generating HTML, which helps when TTFB is long. It does not remove blocking, but shortens the download's critical-path contribution.
Single-page applications. Client-rendered apps often have tiny HTML shells; the blocking problem moves into JavaScript. Server rendering or prerendering is needed before critical CSS makes a difference.
Personalised above-the-fold content. If the first viewport differs by user type (logged-in header, banners), critical CSS must cover all variants, or be generated per template variant.
Validation and Budgeting
Validate in three ways. In the lab, FCP should now occur shortly after the HTML arrives, with no stylesheet requests marked render-blocking. Visually, compare filmstrips: the first painted frame should look complete for above-the-fold content, without unstyled text or layout jumping when the full CSS applies. In the field, compare FCP and LCP p75 before and after, and check CLS did not rise.
Budget three numbers in CI: critical CSS size (for example ≤14KB compressed per template), total CSS size per page, and number of render-blocking requests (target zero or one). These are deterministic and catch regressions early.
| Budget | Target | Why |
|---|---|---|
| Inlined critical CSS | ≤ 14 KB compressed | Fits in early round trips |
| Render-blocking requests | 0–1 | Each one delays first paint |
| Total CSS per page | ≤ 60 KB compressed | Parse cost and download time |
| Unused CSS on key templates | ≤ 40% | Signals framework bloat |
Worked Example: A Publisher Template
A news publisher's article template loaded three stylesheets in the head: a 160KB framework CSS from a CDN subdomain, a 40KB site stylesheet, and a Google Fonts stylesheet. Mobile FCP p75 was 2.6s and LCP p75 3.4s, with TTFB at 0.6s. The team inlined 11KB of critical CSS generated per template, moved the full CSS (purged from 200KB to 52KB) to the main origin and loaded it with the media swap, and self-hosted the fonts with a preload for the body font. FCP p75 dropped to 1.3s and LCP to 2.2s. CLS remained under 0.05 because the critical CSS included layout rules for the article body, which is often visible above the fold on mobile.
Choosing What Goes in Critical CSS
The hardest decision is the boundary of "critical". Too little, and the first paint is visibly unfinished and shifts when the full CSS lands. Too much, and the inlined block grows until it delays the HTML itself. A practical approach is to generate critical CSS for the largest common viewport (for example 1300×900) and the smallest (for example 360×740), take the union, and then inspect what it includes. Layout grid rules, typography, colours, header and navigation, the hero component, and any above-the-fold buttons belong in it. Rules for modals, footers, rarely used components and interaction states (:hover animations) do not.
Per-template critical CSS is more accurate than one site-wide block, but multiplies build work. Sites with a handful of templates (homepage, listing, article, product) generally benefit from per-template generation; sites with many unique layouts may choose a shared critical block covering the common shell, with page-specific components loaded normally.
Measuring the Effect on LCP Specifically
FCP improves almost automatically when blocking CSS is removed; LCP improves only if the LCP element was waiting for that CSS. Check the LCP sub-parts before and after. If the LCP image's load delay was long because it was referenced only from CSS (a background hero), inlining critical CSS that contains the background rule makes it discoverable earlier — but preloading it or converting it to an <img> is better. If render delay dominated, removing blocking CSS should shrink it directly. If load time dominates, critical CSS will not help much; image bytes are the issue.
A Rollout Plan
- Inventory and baseline the top templates by traffic.
- Remove
@importchains and move critical-path CSS to the main origin. - Purge unused CSS from the full stylesheet and verify visually across templates.
- Generate critical CSS per template in the build and inline it, with async loading for the full sheet.
- Release behind a flag to a share of traffic; compare FCP, LCP and CLS in RUM.
- Add CI budgets for critical CSS size and render-blocking count.
Render-Blocking Scripts Deserve the Same Attention
Stylesheets are the usual focus, but synchronous scripts in the head are just as harmful and often easier to fix. A classic <script src> without async or defer stops the HTML parser until the script has downloaded and run, and it also waits for any stylesheet above it. Tag managers, A/B testing snippets, consent managers and polyfill bundles are the common offenders. Audit each one: most can take defer with no behaviour change; analytics can be async; and anti-flicker snippets from experimentation tools, which deliberately hide the page until the experiment loads, should have the shortest possible timeout or be replaced by server-side experiments.
A useful rule is that nothing in the head should be synchronous unless it must run before the first paint and is small and same-origin — for example, a few lines that set a theme class to avoid a dark-mode flash. Everything else belongs after the content, deferred, or loaded on interaction. Measure the result the same way as for CSS: the render-blocking column in the Network panel should be empty, and FCP should sit close to the arrival of the HTML.
Third-Party Stylesheets
Widgets, embedded forms and font services often add their own stylesheets. When they are linked in the head, they block rendering exactly like your own CSS, but they are served from another origin, with a separate connection and a cache policy you do not control. Where possible, self-host the CSS, load widget styles only with the widget (after interaction or when scrolled into view), and move font CSS into your own stylesheet with self-hosted font files. A single slow third-party stylesheet can undo all the gains from critical CSS.
Common Pitfalls
- Critical CSS that drifts. Hand-maintained critical blocks go stale as designs change; generate them in the build.
- Inlining the whole stylesheet. Moves the cost into the HTML and prevents caching.
- Async CSS without noscript fallback. Pages without JavaScript remain unstyled.
- Deferring CSS needed for layout. Causes layout shifts when the full CSS applies.
- Forgetting fonts. A third-party font stylesheet remains blocking even after site CSS is fixed.
FAQ
What is render-blocking CSS?
Stylesheets in the head whose media query matches the current device. The browser does not paint until they have downloaded and been parsed, to avoid showing unstyled content.
How large should critical CSS be?
As small as possible while making the first viewport look complete — commonly under 14KB compressed, often 5–10KB. Larger blocks delay the HTML and cannot be cached separately.
Is inlined CSS cached?
Only as part of the HTML. If HTML is cached at the CDN and in the browser, inlined CSS is too, but repeat visits re-download it with each new HTML response. Keep it small for that reason.
Should scripts in the head use async or defer?
Use defer for scripts that need the DOM and must run in order; use async for independent scripts like analytics. Both remove parser blocking. Module scripts are deferred by default.
Does HTTP/2 make render-blocking CSS irrelevant?
No. HTTP/2 makes many small requests cheaper, but the browser still waits for blocking CSS before painting. The download time on a slow network still matters.
Do frameworks handle critical CSS automatically?
Some do: Nuxt, Angular and Astro have options or integrations for inlining critical styles, and Next.js has experimental support. Verify the output with the Coverage and Network panels.
Can Early Hints replace critical CSS?
Early Hints let the browser start downloading the stylesheet earlier, which helps with slow server responses, but the stylesheet still blocks rendering until it arrives. The two techniques are complementary.
Guides in This Topic
- Extracting critical CSS at build time — automate the critical subset per template.
- Loading non-critical CSS asynchronously — patterns that do not block or flash.
- Removing unused CSS with PurgeCSS — shrink the full stylesheet safely.
- Runtime CSS-in-JS performance cost — when styling libraries add main-thread work.
Related
- Rendering & CSS performance — the full rendering pipeline.
- Measuring LCP with Chrome DevTools — reading render delay in LCP.
- CSS containment & content-visibility — what happens after the first paint.