How to Load Non-Critical CSS Asynchronously

This guide is part of Critical CSS & Render-Blocking Resources, within Rendering & CSS Performance. Once critical CSS is inlined, the full stylesheet must still load — but without blocking the first paint. HTML has no async attribute for stylesheets, so several patterns exist: swapping a non-matching media attribute, preloading and switching rel, splitting CSS by media query, and loading stylesheets from JavaScript. They differ in browser behaviour, priority, and how they fail.

The main risk is not technical failure but visual: if the async stylesheet contains rules for content visible on first paint, users see a flash of unstyled content (FOUC) and layout shifts when it applies. Async loading is only safe when paired with complete critical CSS, or when the deferred CSS really applies to content below the fold or in later interactions.

Async CSS loading patterns Comparison of patterns for loading CSS without blocking rendering, by priority, JavaScript dependency and fallback behaviour. Async CSS loading patterns Pattern Fetch priority Needs JS Fallback media=print + onload swap lowest yes for swap noscript link rel=preload + onload rel swap high yes noscript link Split by media query low for non-matching no native JS-injected link varies yes none

Rapid Diagnosis

  • Check stylesheet links in the head: any without a non-matching media query and without an async pattern are blocking.
  • Watch the filmstrip in a lab run: unstyled content followed by a styled frame indicates async CSS that should be critical.
  • Check CLS attribution for shifts that coincide with a stylesheet finishing.
  • Check stylesheet priority in the Network panel; very-low-priority CSS may arrive late on congested connections.

Root Cause Analysis

1. No native async for CSS. All patterns are workarounds with trade-offs.

2. Incomplete critical CSS. Async CSS that styles above-the-fold content causes FOUC.

3. Low priority. The media swap uses print priority, so the stylesheet can be delayed behind images.

4. JavaScript dependency. Most patterns rely on an onload handler to apply the stylesheet.

Step-by-Step Resolution

1. Use the media swap for most cases

html
<link rel="stylesheet" href="/css/site.4f2a1c.css" media="print" onload="this.media='all'; this.onload=null">
<noscript><link rel="stylesheet" href="/css/site.4f2a1c.css"></noscript>
<!-- trade-off: print-media stylesheets get the lowest fetch priority, so on busy
     pages the full CSS may arrive after images; add a preload if it is needed soon. -->

2. Use preload when the stylesheet is needed quickly

html
<link rel="preload" href="/css/site.4f2a1c.css" as="style" onload="this.onload=null; this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/site.4f2a1c.css"></noscript>
<!-- trade-off: high priority competes with the LCP image for bandwidth; use it for
     stylesheets that style content seen within the first scroll. -->

3. Split by media query where possible

html
<link rel="stylesheet" href="/css/base.css">
<link rel="stylesheet" href="/css/desktop.css" media="(min-width: 1024px)">
<link rel="stylesheet" href="/css/print.css" media="print">

Non-matching stylesheets are downloaded at low priority without blocking. No JavaScript is needed.

4. Defer component CSS until it is needed

Styles for modals, carousels further down the page, or account menus can be loaded when the component is first used (code-split CSS in modern bundlers does this automatically for lazily loaded components).

Async CSS priority vs LCP image on a slow connection Timeline showing how a preloaded stylesheet competes with the LCP image while a media-swapped one downloads after it. Async CSS priority vs LCP image on a slow connection preload swap site.css (high) LCP image shares bandwidth media=print swap LCP image site.css (lowest) 0ms 300ms 600ms 900ms 1200ms 1500ms 1800ms 2100ms 2400ms LCP

Verification

In the Network panel, the full stylesheet should not be marked render-blocking, and should load at the expected priority. FCP should occur before the stylesheet finishes. Watch the filmstrip and CLS: no unstyled frames, no shifts when the stylesheet applies. Test with JavaScript disabled to confirm the noscript fallback works.

Worked Example: A Marketing Site

A marketing site switched its 90KB stylesheet to rel=preload with an onload swap after inlining 8KB of critical CSS. FCP improved by 700ms, but LCP improved by only 150ms: the high-priority stylesheet competed with the hero image. Switching to the media swap (lowest priority) improved LCP by a further 280ms, because the hero image now had the bandwidth to itself, while the full stylesheet still arrived before users scrolled. A CLS check confirmed no shifts, since all above-the-fold rules were in the critical block.

Choosing an async CSS pattern Decision sequence for picking how to load a stylesheet without blocking the first paint. Choosing an async CSS pattern Does the CSS style above-the-fold content? Inline it as critical CSS instead yes no Is it needed within the first scroll? Preload with onload rel swap yes no Does it apply only at some breakpoints? Split it into media-query stylesheets yes no Media swap (print to all) with a noscript fallback

When Not to Load CSS Asynchronously

Async loading is a pairing: it only works with complete critical CSS. If you cannot generate reliable critical CSS — for example, highly dynamic pages where above-the-fold content varies widely — a single small, cached, same-origin, render-blocking stylesheet may be the better choice. A 25KB compressed stylesheet on the same HTTP/2 connection as the HTML often costs only a round trip, and avoids FOUC risk entirely. Measure: if the gap between TTFB and FCP is already small, the async complexity is not worth it.

A CSP-Friendly Loader

Inline onload attributes conflict with a strict Content Security Policy. A tiny external script avoids them and handles every deferred stylesheet consistently:

javascript
// /js/async-css.js, loaded with defer.
for (const link of document.querySelectorAll('link[data-async-css]')) {
  const apply = () => { link.media = link.dataset.media || 'all'; };
  if (link.sheet) apply(); else link.addEventListener('load', apply, { once: true });
}
// trade-off: a deferred script runs after parsing, so the swap happens slightly
// later than an inline onload; the stylesheet itself still downloads early.

Mark stylesheets with media="print" data-async-css in the HTML. Because the check for link.sheet handles stylesheets that finished before the script ran, no load event is missed.

Measuring Whether Async Loading Paid Off

Compare three timings before and after the change, on the same throttled profile: FCP, LCP and the moment the full stylesheet finishes (from Resource Timing). FCP should move earlier by roughly the stylesheet's former download time. LCP should move earlier too, unless the LCP image was the bottleneck — in which case check that the stylesheet is not now competing with it. The stylesheet's own finish time matters for users who interact quickly: if it arrives after a typical first click, interactive elements styled only by the full CSS may look broken briefly. RUM can capture this by logging the stylesheet's responseEnd alongside the first interaction time.

Common Mistakes

  • Forgetting the noscript fallback. Pages without JavaScript remain unstyled.
  • Leaving onload active. Some browsers re-fire load when media changes; clear the handler.
  • Async-loading CSS for visible content. FOUC and CLS.
  • Preloading every stylesheet. Turns them all into high-priority downloads competing with the LCP image.

Edge Cases

Content Security Policy. Inline onload handlers require 'unsafe-inline' or hashes; use a small external script that swaps media for all link[data-async] elements instead.

Print styles. If you use the media swap on the main stylesheet, print a page to confirm print styles still apply as intended.

Back-forward cache. Restored pages keep their applied stylesheets; no special handling needed.

SPAs. Bundlers inject CSS for lazy routes asynchronously; ensure route CSS loads before the route renders to avoid FOUC on navigation.

FAQ

Why use media="print" to load CSS asynchronously?

Stylesheets for non-matching media are downloaded without blocking rendering. Switching the media to all once loaded applies the styles.

Is the preload pattern better than the media swap?

Preload gives higher priority, so the stylesheet arrives sooner, but it competes with critical resources such as the LCP image. Choose based on how soon the CSS is needed.

Is loadCSS still needed?

The loadCSS library popularised these patterns. Modern browsers support preload and onload on link elements natively, so a library is generally unnecessary.

Can I make CSS async without JavaScript?

Only by splitting stylesheets by media query, which loads non-matching sheets without blocking. Full async application of styles needs a small amount of JavaScript.

Will async CSS hurt CLS?

Only if it styles content already visible. With complete critical CSS, async loading should not cause shifts.

Does async CSS help INP?

Indirectly at most. Parsing a large stylesheet after load can cause a long task that coincides with early interactions; smaller CSS helps more than loading strategy.