Skip to main content

Reduce stylesheet count

Minimize the number of separate stylesheet links on a page to reduce expensive browser style recalculations that block user interactions and hurt INP.


Each separate stylesheet link requires browser parsing and application. URLs are downloaded only once, but the browser must parse and apply each stylesheet separately, which causes expensive style recalculations that severely impact INP (Interaction to Next Paint).

On collection pages with many product cards, each card rendering its own stylesheet can result in 100 to 1,150 separate stylesheets, causing style recalculation to take hundreds of milliseconds. This blocks interactions like opening menus, clicking products, or switching variants.

Measured improvements from production themes show that reducing stylesheets from 1,150 to 30 produced a 98 percent improvement in style recalculation time. Style recalculations happen throughout the page lifecycle, during HTML parsing, script execution, and especially during interactions, so fast recalculations are critical for good INP scores.


Anchor to Add ,[object Object], parameter to snippetsAdd skip_styles parameter to snippets

For snippets rendered multiple times, such as product cards, loading spinners, and progress bars, add a skip_styles parameter:

snippets/card-product.liquid

{%- unless skip_styles -%}
<link rel="stylesheet" href="{{ 'component-card.css' | asset_url }}">
{%- endunless -%}

<div class="card-product">
{%- comment -%} Card markup {%- endcomment -%}
</div>

Usage in sections:

{%- assign skip_card_product_styles = false -%}
{%- for product in collection.products limit: 12 -%}
{% render 'card-product', product: product, skip_styles: skip_card_product_styles %}
{%- assign skip_card_product_styles = true -%}
{%- endfor -%}

Result: 12 product cards load 1 stylesheet instead of 12, and 50 product cards load 1 stylesheet instead of 50.

Render arguments can't contain comparisons

Liquid parses render arguments as name-value pairs, so a comparison like skip_styles: forloop.index > 1 binds skip_styles to forloop.index and silently drops the > 1. The snippet receives a number, and every number is truthy in Liquid, so {% unless skip_styles %} never runs and the stylesheet never loads. Assign the comparison to a variable first, then pass the variable.

Anchor to Move global snippet styles to ,[object Object]Move global snippet styles to base.css

For snippets used across the entire theme, such as loading spinners, progress bars, and icons, move their CSS to base.css instead of including stylesheet links.

Candidates include:

  • Loading spinners.
  • Progress bars.
  • Icon systems.
  • Common UI components.
  • Any snippet used site-wide.

Instead of separate CSS files for related components, combine them into single consolidated stylesheets.

Benefits:

  • Better compression, because gzip and brotli are more effective on larger files.
  • Fewer requests.
  • Faster parsing, because one larger file is faster than many small files.

Anchor to Real-world impact from the Trade themeReal-world impact from the Trade theme

Before optimization: more than 1,150 separate stylesheet links, with duplicate component-card.css for every card and duplicate loading-spinner.css throughout the page.

After optimization: about 30 stylesheets, with base.css including the spinner and progress bar, component-card.css loaded once, and only unique stylesheets loaded.

Performance impact: style recalculation 98 percent faster, INP significantly improved, and menu opens and clicks no longer blocked.

Anchor to Product card snippet with ,[object Object]Product card snippet with skip_styles

snippets/card-product.liquid

{%- liquid
unless skip_styles
echo 'component-card.css' | asset_url | stylesheet_tag
echo 'component-price.css' | asset_url | stylesheet_tag
endunless
-%}

<div class="card-product">
<a href="{{ product.url }}">
{{ product.featured_image
| image_url: width: 600
| image_tag: loading: image_loading
}}
<h3>{{ product.title }}</h3>
<p class="price">{{ product.price | money }}</p>
</a>
</div>

Section using the snippet:

{% paginate collection.products by 12 %}
<div class="product-grid">
{%- assign skip_card_product_styles = false -%}
{%- for product in collection.products limit: 12 -%}
{%- assign image_loading = 'lazy' -%}
{%- if forloop.index <= 4 -%}
{%- assign image_loading = 'eager' -%}
{%- endif -%}

{% render 'card-product',
product: product,
image_loading: image_loading,
skip_styles: skip_card_product_styles
%}

{%- assign skip_card_product_styles = true -%}
{%- endfor -%}
</div>
{% endpaginate %}

Result: 12 cards load 1 stylesheet instead of 12. The first card loads styles, and the remaining 11 cards skip styles.

The loading argument of image_tag takes the string 'eager' or 'lazy'. image_tag copies it into the attribute as-is, so passing a boolean or a number emits an invalid loading="true" or loading="1". If you omit loading, then image_tag sets it for you based on the section's position on the page.

Anchor to Product cards in collectionsProduct cards in collections

Problem: 50 product cards load 50 or more stylesheets.

Solution:

{% paginate collection.products by 50 %}
{%- assign skip_card_product_styles = false -%}
{% for product in collection.products limit: 50 %}
{% render 'card-product',
product: product,
skip_styles: skip_card_product_styles
%}
{%- assign skip_card_product_styles = true -%}
{% endfor %}
{% endpaginate %}

Result: 50 product cards load 1 stylesheet.

Anchor to Global UI componentsGlobal UI components

Problem: a loading spinner snippet with a stylesheet is used in the cart, product page, search, and other places.

Solution: move the loading-spinner.css contents to base.css and update the snippet to remove the stylesheet link.

Problem: separate files for product-card.css, product-price.css, product-badge.css, and product-rating.css.

Solution: consolidate into a single component-product.css file with all related component styles.


Measure stylesheet count with console:

document.querySelectorAll("link[rel=stylesheet]").length

Targets:

  • Collection page: fewer than 50 stylesheets.
  • Product page: fewer than 30 stylesheets.
  • Homepage: fewer than 40 stylesheets.

Problematic:

  • Collection page: more than 100 stylesheets.
  • Product page: more than 50 stylesheets.

Chrome DevTools Performance panel: record an interaction and look for Recalculate Style events, then compare before and after.

INP measurement: use the Chrome DevTools Performance panel to record interactions and check the INP metric in the summary. Target under 200 ms.

Visual inspection: use the Chrome DevTools Coverage tab to identify unused CSS and duplicate stylesheets loading the same rules.



Was this page helpful?