Fixing Layout Shift (CLS) on Shopify Collections

Your Shopify collection pages fail Core Web Vitals on CLS, and the page visibly jumps while loading.
Cumulative Layout Shift measures content moving after it has been painted. On collection pages there are usually three culprits, and they compound: each one shifts everything below it.
Symptom
- PageSpeed Insights reports CLS above 0.1 (the "good" threshold) on collection templates.
- Product cards jump downward as images load.
- Text reflows once the web font arrives.
- A promotional bar or cookie banner appears after render and pushes the page down.
- Tapping a product occasionally opens the wrong one, because the grid moved mid-tap.
Cause
1. Images without reserved space
An <img> with no dimensions occupies zero height until it loads, then suddenly claims its full height. In a grid of 24 products this happens 24 times.
2. Font swap
A web font loading with font-display: swap renders fallback text first, then re-renders in the real font. If the metrics differ, every line reflows.
3. Injected elements
App embeds, announcement bars and consent banners inserted at the top of the DOM push the entire page down after paint. This is the largest single contributor on most stores.
Fix
1. Reserve image space with width, height and aspect-ratio
{%- comment -%}
width/height give the browser an aspect ratio before the file arrives, so the
space is reserved from first paint.
{%- endcomment -%}
<img
src="{{ product.featured_image | image_url: width: 600 }}"
srcset="
{{ product.featured_image | image_url: width: 400 }} 400w,
{{ product.featured_image | image_url: width: 600 }} 600w,
{{ product.featured_image | image_url: width: 900 }} 900w
"
sizes="(min-width: 990px) 25vw, (min-width: 750px) 33vw, 50vw"
width="{{ product.featured_image.width }}"
height="{{ product.featured_image.height }}"
alt="{{ product.featured_image.alt | escape }}"
loading="lazy"
>
Then lock the ratio in CSS so mixed portrait/landscape products do not vary:
.product-card__image {
aspect-ratio: 1 / 1;
width: 100%;
height: auto;
object-fit: cover;
}
One caveat: do not set loading="lazy" on images above the fold. It delays the LCP image and trades a CLS win for an LCP loss.
2. Stop the font from reflowing
<link
rel="preload"
as="font"
type="font/woff2"
href="{{ 'yourfont.woff2' | asset_url }}"
crossorigin
>
@font-face {
font-family: 'YourFont';
src: url('yourfont.woff2') format('woff2');
font-display: optional; /* no swap, no reflow */
size-adjust: 96%; /* tune so fallback metrics match */
}
font-display: optional is the strongest anti-CLS setting: if the font is not ready almost immediately, the browser keeps the fallback for that page load rather than swapping mid-render.
3. Reserve space for injected elements
If a banner will appear, hold its height from the start:
.announcement-bar-slot {
min-height: 44px; /* the banner's real height */
}
@media (max-width: 749px) {
.announcement-bar-slot { min-height: 60px; } /* wraps to two lines */
}
An empty 44px strip is invisible to users and costs nothing. A 44px jump costs you the metric.
For consent banners, overlay rather than push:
.consent-banner {
position: fixed;
inset-inline: 0;
bottom: 0;
z-index: 100;
}
A fixed-position element is outside normal flow and contributes no CLS at all.
Verify
Measure in the field, not just the lab — CLS is cumulative over the page's life, so a shift on scroll still counts:
- Chrome DevTools → Performance → tick "Web Vitals", record, and scroll the full collection.
- Look for the red CLS markers and expand each to see which node moved.
- PageSpeed Insights on a real collection URL, not the homepage.
- After deploying, check Search Console → Core Web Vitals for field data — it lags ~28 days.
// Log every shift and the element responsible, in the console.
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.hadRecentInput) continue; // user-initiated shifts do not count
console.log('shift', entry.value.toFixed(4), entry.sources?.map(s => s.node));
}
}).observe({ type: 'layout-shift', buffered: true });
Prevent
- Every
<img>getswidthandheight. Make it a review rule. - Audit app embeds before installing. Anything that injects at the top of
<body>is a CLS risk. - Re-test after every app install, not just after theme changes.
- Watch collection pages specifically — they repeat any per-card mistake dozens of times.
Frequently asked questions
Does CLS affect rankings?
It is part of Core Web Vitals, which is a ranking signal — but a modest one. The stronger argument is conversion: a grid that jumps while someone is tapping loses the sale.
Why does my lab score pass but field data fail?
Lab tests load once at a fixed viewport. Real users scroll, trigger lazy-loaded content and see consent banners. Always trust field data.
Can I fix CLS caused by a third-party app?
Usually by reserving space for it, as above. If the app injects unpredictable heights, that is worth raising with the developer — or replacing the app.
Related reading
- CSS or JS changes not showing?
- Traffic but no sales? Start here
- Shopify speed optimization — our service page
Need a hand?
We optimise Shopify store performance for merchants in the USA, UAE, Saudi Arabia, Qatar and the UK. If Core Web Vitals are failing, get a free audit or see our Shopify speed optimization service.
The Yaxify Team
Shopify & Web Development Agency
We build custom Shopify stores and high-performance websites for growing businesses across the USA, UK, and UAE. Everything we publish comes out of client work we have actually shipped.
More about Yaxify →