---
title: Load critical CSS synchronously
description: >-
  Load critical CSS synchronously and apply the async CSS pattern only to
  sections outside the initial viewport after testing confirms it's beneficial.
source_url:
  html: >-
    https://shopify.dev/docs/storefronts/themes/best-practices/performance/load-critical-css-synchronously
  md: >-
    https://shopify.dev/docs/storefronts/themes/best-practices/performance/load-critical-css-synchronously.md
api_name: liquid
---

# Load critical CSS synchronously

Load critical CSS synchronously. Apply the async CSS pattern, where `media="print"` with `onload` switches the media to `all`, only to sections outside the initial viewport after testing proves it's beneficial.

***

## Why

CSS is render-blocking by design, to prevent a jarring Flash of Unstyled Content (FOUC). When async CSS loads for content visible in the initial viewport:

* Content renders with missing styles.
* Styles arrive late and cause re-renders.
* Content shifts to accommodate new styles, which increases CLS.
* FOUC confuses users.
* It can trigger unnecessary image loads, because of `IntersectionObserver` confusion.

The goal isn't to eliminate all render-blocking CSS, but to be strategic about what blocks rendering. Critical CSS is the minimum set of styles required to render the content visible in the initial viewport correctly, and it should always be render-blocking.

Rule: if the stylesheet styles anything users see before scrolling, then load it synchronously (render-blocking). Defer only non-critical styles for content outside the initial viewport.

***

## How

The async CSS pattern works like this:

1. The browser sees `media="print"`.
2. It sets download priority to Lowest.
3. Initial render doesn't use these styles.
4. After the load event, JavaScript changes `media` to `all`.
5. Priority changes to Highest.
6. The page re-renders with the new styles.

Load critical CSS for content visible in the initial viewport synchronously (render-blocking). Defer non-critical CSS for content outside the initial viewport asynchronously only if testing proves it's beneficial.

Use [`section.index`](https://shopify.dev/docs/api/liquid/objects/section#section-index) to conditionally apply async CSS:

```liquid
{% unless section.index > 3 %}
  {%- comment -%} Initial viewport, and any context where section.index is nil: render-blocking {%- endcomment -%}
  <link rel="stylesheet" href="{{ 'section.css' | asset_url }}">
{% else %}
  {%- comment -%} Outside the initial viewport: async {%- endcomment -%}
  <link rel="stylesheet" href="{{ 'section.css' | asset_url }}" media="print" onload="this.media='all'">
  <noscript><link rel="stylesheet" href="{{ 'section.css' | asset_url }}"></noscript>
{% endunless %}
```

The condition tests `section.index > 3` rather than `section.index <= 3`. `section.index` is `nil` in the online store editor, in static sections, and in the [Section Rendering API](https://shopify.dev/docs/api/section-rendering), and comparisons against `nil` are falsey, so `{% if section.index <= 3 %}` would load the stylesheet asynchronously in those contexts. Writing the async case as the positive test keeps `nil` on the synchronous side. For the full explanation, refer to [Load above-the-fold and below-the-fold sections differently with `section.index`](https://shopify.dev/docs/storefronts/themes/best-practices/performance/use-section-index).

For sections where you're unsure whether async loading is safe, a conservative approach is to place the `<link rel="stylesheet">` tag immediately before the section's content. The browser can render everything above that point while the preload scanner picks up the stylesheet.

***

## Examples

### Real-world: Dawn theme CLS improvement

As a real-world example, the Dawn theme had async CSS applied to components visible in the initial viewport. When this was discovered in [PR #2270](https://github.com/Shopify/dawn/pull/2270), it was causing visible layout shifts:

> "You may notice the whole 'Obsessive Attention. Intelligent Effort.' shifting to the right in the control run because of asynchronous `component-rte.css`."

Removing async CSS from content visible in the initial viewport improved CLS by 95 percent, from 0.065 to near-zero. The async pattern had been applied in too many places throughout the theme, causing unstyled content to appear and shift when styles loaded. This is especially problematic on first visits and on slower connections.

### Async CSS pattern

```html
<link
  rel="stylesheet"
  href="style.css"
  media="print"
  onload="this.media='all'"
/>
<noscript><link rel="stylesheet" href="style.css" /></noscript>
```

### When to use

* Sections outside the initial viewport, section 4 and later.
* Non-critical styling.
* After testing proves it's beneficial.

### When not to use

* The first 3 sections.
* Content visible in the initial viewport.
* Critical page styling.
* Without before-and-after testing.

***

## Testing

Before implementing the async CSS pattern:

1. Open the **Insights** tab in the [Chrome DevTools Performance panel](https://developer.chrome.com/docs/devtools/performance).
2. Record a page load, and note CLS and visual rendering. Use the **View Trace** button for details.
3. Implement async CSS.
4. Record again and compare FCP, LCP, and CLS.
5. Check RUM data after deployment.

Use the **Performance** panel filmstrip view to see visual shifts. Compare with and without the async pattern. Check CLS in RUM data after deployment. The pattern might hurt more than it helps, so always verify.

***

## References

* [Dawn PR #2270](https://github.com/Shopify/dawn/pull/2270): removed async CSS from content visible in the initial viewport (95 percent CLS improvement).
* [`if`](https://shopify.dev/docs/api/liquid/tags/if) tag
* [`section.index`](https://shopify.dev/docs/api/liquid/objects/section#section-index)
* [`asset_url`](https://shopify.dev/docs/api/liquid/filters/asset_url) filter
* [How to optimize Cumulative Layout Shift (CLS) on Shopify sites](https://performance.shopify.com/blogs/blog/how-to-optimize-cumulative-layout-shift-cls-on-shopify-sites)
* [How layout position impacts three big web performance levers](https://performance.shopify.com/blogs/blog/how-layout-position-impacts-three-big-web-performance-levers)
* [Announcing new Liquid features for better web performance](https://performance.shopify.com/blogs/blog/announcing-new-liquid-features-for-better-web-performance)
* [Defer non-critical resources](https://shopify.dev/docs/storefronts/themes/best-practices/performance/defer-non-critical-resources)
* [Use `section.index` for position-aware loading](https://shopify.dev/docs/storefronts/themes/best-practices/performance/use-section-index)
* [Never lazy-load the LCP image](https://shopify.dev/docs/storefronts/themes/best-practices/performance/never-lazy-load-lcp-image)
* [Reduce stylesheet count](https://shopify.dev/docs/storefronts/themes/best-practices/performance/reduce-stylesheet-count)

***
