Skip to main content

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.


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.


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 to conditionally apply async CSS:

{% 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, 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.

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.


Anchor to Real-world: Dawn theme CLS improvementReal-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, 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.

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

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

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

Before implementing the async CSS pattern:

  1. Open the Insights tab in the Chrome DevTools Performance panel.
  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.



Was this page helpful?