Cumulative Layout Shift (CLS) measures visual stability by tracking unexpected layout shifts during page load. This guide shows you how to fix CLS issues in your CS-Cart store.
What is CLS?
CLS quantifies how much visible content shifts unexpectedly during the page lifecycle. Common causes include:
- Images without dimensions
- Ads, embeds, or iframes without dimensions
- Dynamically injected content
- Web fonts causing FOIT/FOUT
- Actions waiting for network response
CLS Scoring
- Good: 0-0.1 (green)
- Needs Improvement: 0.1-0.25 (orange)
- Poor: Over 0.25 (red)
Goal: Achieve CLS under 0.1 for 75% of page views.
Diagnosing CLS Issues
Use PageSpeed Insights
- Go to PageSpeed Insights
- Enter your CS-Cart store URL
- Check Cumulative Layout Shift metric
- Expand Diagnostics to see "Avoid large layout shifts"
- Review elements causing shifts
Use Chrome DevTools
Experience Panel:
- Open DevTools (F12)
- Press
Ctrl+Shift+P(Cmd+Shift+P on Mac) - Type "Show Rendering"
- Enable Layout Shift Regions
- Reload page and watch for blue highlights
Performance Panel:
- Open Performance tab
- Click Record
- Reload page
- Stop recording
- Look for Layout Shift events (red bars)
- Click on shifts to see details
Measure CLS with JavaScript
let clsValue = 0;
let clsEntries = [];
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value;
clsEntries.push(entry);
console.log('Layout shift:', entry.value, entry.sources);
}
}
console.log('Current CLS:', clsValue);
});
observer.observe({type: 'layout-shift', buffered: true});
Common CLS Issues in CS-Cart
1. Images Without Dimensions
Problem: Images load and push content down.
Solution: Always specify width and height attributes.
Product Images:
File: design/themes/[theme]/templates/blocks/product_list_templates/grid.tpl
{* Before - causes layout shift *}
<img src="{$product.main_pair.icon.image_path}"
alt="{$product.product|escape}">
{* After - reserves space *}
<img src="{$product.main_pair.icon.image_path}"
alt="{$product.product|escape}"
width="{$product.main_pair.icon.image_x}"
height="{$product.main_pair.icon.image_y}">
Responsive Images with Aspect Ratio:
{* Calculate aspect ratio *}
{assign var="aspect_ratio" value=$product.main_pair.icon.image_y/$product.main_pair.icon.image_x*100}
<div style="position: relative; width: 100%; padding-bottom: {$aspect_ratio}%;">
<img src="{$product.main_pair.icon.image_path}"
alt="{$product.product|escape}"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">
</div>
Using CSS aspect-ratio:
.product-image {
aspect-ratio: 1 / 1; /* For square images */
width: 100%;
height: auto;
}
<img src="{$product.main_pair.icon.image_path}"
alt="{$product.product|escape}"
class="product-image"
width="300"
height="300">
2. Dynamic Content Loading
Problem: Content loads after page render, pushing existing content.
Banner Blocks:
{* Reserve space for banner *}
<div class="banner-container" style="min-height: 400px;">
{include file="blocks/banner.tpl"}
</div>
Product Blocks:
/* Reserve space before products load */
.ty-grid-list {
min-height: 600px;
}
.ty-grid-list__item {
min-height: 350px;
}
AJAX-Loaded Content:
// Before loading content, set container height
var container = document.getElementById('ajax-container');
container.style.minHeight = '500px';
// Load content
$.ajax({
url: 'content.html',
success: function(data) {
container.innerHTML = data;
container.style.minHeight = ''; // Remove after loaded
}
});
3. Web Fonts Causing FOUT/FOIT
Problem: Text shifts when web fonts load.
Solution 1: Use font-display: swap
@font-face {
font-family: 'YourFont';
src: url('/fonts/your-font.woff2') format('woff2');
font-display: swap; /* Show fallback immediately, swap when loaded */
}
Solution 2: Preload Fonts
<link rel="preload"
href="/fonts/your-font.woff2"
as="font"
type="font/woff2"
crossorigin>
Solution 3: Font Loading API
// Load font before showing text
if ('fonts' in document) {
Promise.all([
document.fonts.load('1rem YourFont'),
document.fonts.load('bold 1rem YourFont')
]).then(() => {
document.body.classList.add('fonts-loaded');
});
}
/* Match fallback font metrics */
body {
font-family: Arial, sans-serif;
}
body.fonts-loaded {
font-family: 'YourFont', Arial, sans-serif;
}
4. Ads and Embeds
Problem: Ad slots without dimensions cause shifts.
Google AdSense:
<!-- Reserve exact space -->
<div style="width: 728px; height: 90px;">
<!-- Ad code here -->
</div>
Responsive Ads:
/* Use aspect ratio boxes */
.ad-container {
position: relative;
width: 100%;
padding-bottom: 12.37%; /* 90/728 for 728x90 banner */
}
.ad-container > * {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
YouTube/Video Embeds:
<!-- Maintain 16:9 aspect ratio -->
<div style="position: relative; padding-bottom: 56.25%; height: 0;">
<iframe src="https://www.youtube.com/embed/..."
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"
frameborder="0"
allowfullscreen></iframe>
</div>
5. Notification Banners
Problem: Cookie consent or promotional banners pushing content.
Reserve Space at Top:
/* Reserve space for cookie banner */
body.has-cookie-banner {
padding-top: 60px;
}
.cookie-banner {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 60px;
z-index: 9999;
}
Or Use Overlay Instead of Pushing:
.notification-banner {
position: fixed;
bottom: 0; /* Don't push content */
left: 0;
right: 0;
z-index: 9999;
}
6. CS-Cart Product Options
Problem: Product options loading and expanding content.
File: design/themes/[theme]/templates/views/products/components/product_options.tpl
/* Reserve space for product options */
.ty-product-options {
min-height: 150px; /* Adjust based on typical option height */
}
Skeleton Loading:
/* Show skeleton while options load */
.ty-product-options.loading {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
7. Lazy-Loaded Images
Problem: Images appearing causes content shift.
Solution: Placeholder with Same Dimensions
{* Placeholder approach *}
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3C/svg%3E"
data-src="{$product.main_pair.icon.image_path}"
alt="{$product.product|escape}"
width="300"
height="300"
class="lazy">
Using Intersection Observer:
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.remove('lazy');
imageObserver.unobserve(img);
}
});
});
document.querySelectorAll('img.lazy').forEach(img => {
imageObserver.observe(img);
});
8. CS-Cart Filters and Sidebar
Problem: Sidebar loading causes content shift.
/* Reserve sidebar space */
.ty-mainbox-container {
display: grid;
grid-template-columns: 250px 1fr; /* Fixed sidebar width */
}
@media (max-width: 768px) {
.ty-mainbox-container {
grid-template-columns: 1fr; /* Stack on mobile */
}
}
9. Price Updates
Problem: Prices loading or updating via AJAX.
/* Reserve space for price */
.ty-price {
min-height: 30px;
display: block;
}
.ty-price-num {
display: inline-block;
min-width: 60px; /* Prevent width changes */
}
10. Star Ratings
Problem: Rating stars loading causes shift.
/* Reserve space for ratings */
.ty-product-review-stars {
height: 20px;
display: block;
}
CS-Cart-Specific Fixes
Fix Product Grid Layout
File: design/themes/[theme]/css/tygh/grid.less
// Fixed grid item height
.ty-grid-list__item {
min-height: 400px; // Adjust to your needs
.ty-grid-list__image {
height: 250px;
display: flex;
align-items: center;
justify-content: center;
img {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
}
}
.ty-grid-list__item-name {
min-height: 40px; // Reserve space for product name
}
}
Fix Category Banner Shifts
{* File: design/themes/[theme]/templates/categories/view.tpl *}
{if $category_data.main_pair.detailed.image_path}
<div class="category-banner" style="aspect-ratio: 16/9; max-height: 400px;">
<img src="{$category_data.main_pair.detailed.image_path}"
alt="{$category_data.category|escape}"
width="1600"
height="900"
style="width: 100%; height: 100%; object-fit: cover;">
</div>
{/if}
Fix Header Logo Shift
/* Reserve exact logo space */
.ty-logo {
width: 200px; /* Your logo width */
height: 60px; /* Your logo height */
}
.ty-logo img {
width: 100%;
height: 100%;
object-fit: contain;
}
Fix Add to Cart Button
/* Prevent button size changes */
.ty-btn-add-to-cart {
min-width: 150px;
height: 40px;
padding: 0 20px;
}
/* Loading state */
.ty-btn-add-to-cart.loading {
/* Same dimensions during loading */
}
Advanced Techniques
Use transform Instead of top/left
Bad (causes layout shift):
.element {
position: relative;
top: 20px; /* Causes layout shift */
}
Good (doesn't cause layout shift):
.element {
transform: translateY(20px); /* No layout shift */
}
Reserve Space with CSS Grid
.product-page {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto auto;
gap: 20px;
}
.product-images {
grid-column: 1;
grid-row: 1 / 3;
min-height: 600px; /* Reserve space */
}
.product-info {
grid-column: 2;
grid-row: 1;
}
Skeleton Screens
Create placeholder content that matches loaded content dimensions:
<!-- Skeleton for product block -->
<div class="product-skeleton">
<div class="skeleton-image" style="height: 250px; background: #f0f0f0;"></div>
<div class="skeleton-title" style="height: 20px; background: #f0f0f0; margin-top: 10px;"></div>
<div class="skeleton-price" style="height: 24px; width: 60%; background: #f0f0f0; margin-top: 10px;"></div>
</div>
Animations Without Layout Shift
/* Fade in without shifting */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.product-item {
animation: fadeIn 0.3s;
/* Don't animate height, width, margin, or padding */
}
Testing and Monitoring
Test in Real Devices
CLS often varies by:
- Device (mobile vs. desktop)
- Connection speed
- Browser
Test on:
- Mobile devices (actual devices, not just emulation)
- Slow 3G connections
- Different browsers (Chrome, Safari, Firefox)
Continuous Monitoring
Google Analytics 4 Web Vitals:
import {getCLS} from 'https://unpkg.com/web-vitals@3/dist/web-vitals.js?module';
getCLS((metric) => {
gtag('event', 'web_vitals', {
event_category: 'Web Vitals',
event_label: metric.id,
value: Math.round(metric.value * 1000),
metric_name: 'CLS',
non_interaction: true
});
});
Chrome User Experience Report
Check your site's field data:
- CrUX Dashboard
- Enter your domain
- View CLS scores from real users
Quick Fixes Checklist
- Add width and height to all images
- Use CSS aspect-ratio for responsive images
- Set min-height on dynamic content containers
- Use font-display: swap for web fonts
- Preload critical fonts
- Reserve space for ads and embeds
- Fix notification banner behavior
- Set fixed dimensions for product grid items
- Reserve space for product options
- Use transform instead of top/left for animations
- Implement skeleton loading for AJAX content
- Test on mobile devices
- Monitor CLS with RUM
Common Mistakes to Avoid
1. Don't Animate Layout Properties
Bad:
.button:hover {
width: 200px; /* Causes layout shift */
height: 50px; /* Causes layout shift */
}
Good:
.button {
width: 200px;
height: 50px;
}
.button:hover {
transform: scale(1.05); /* No layout shift */
}
2. Don't Insert Content Above Existing Content
Bad:
// Inserting banner at top pushes everything down
document.body.insertBefore(banner, document.body.firstChild);
Good:
// Reserve space from the beginning
// Or use fixed positioning
3. Don't Load Critical Resources Too Late
Bad:
<!-- Critical font loaded late -->
<link rel="stylesheet" href="fonts.css">
Good:
<!-- Preload critical font -->
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
Troubleshooting
CLS Still High After Fixes
Check:
- Test on actual mobile device (not just emulation)
- Test with slow network (3G throttling)
- Disable browser cache
- Check third-party scripts (ads, analytics)
- Review AJAX-loaded content
Use Layout Shift Recorder:
// Record all layout shifts
const shifts = [];
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
shifts.push({
value: entry.value,
sources: entry.sources.map(s => s.node),
time: entry.startTime
});
}
}
}).observe({type: 'layout-shift', buffered: true});
// After page load, inspect shifts
console.table(shifts);
Different CLS Scores
If PageSpeed Insights shows different scores than field data:
- Lab data (PageSpeed) uses simulated environment
- Field data (CrUX) shows real user experience
- Focus on improving field data
Layout Shifts Only on Mobile
Common causes:
- Responsive images without proper sizing
- Mobile-specific ads or banners
- Different font rendering on mobile
- Touch-specific interactions