PrestaShop CLS Optimization
Reduce Cumulative Layout Shift (CLS) on your PrestaShop store to improve Core Web Vitals scores, enhance user experience, and prevent frustrating layout jumps.
Understanding CLS in PrestaShop
What is CLS?
Cumulative Layout Shift (CLS) measures visual stability by quantifying unexpected layout shifts during page load. Every time a visible element changes position, it contributes to the CLS score.
CLS Scoring Thresholds
| Score | Range | User Experience |
|---|---|---|
| Good | ≤ 0.1 | Stable, excellent |
| Needs Improvement | 0.1 - 0.25 | Some shifting |
| Poor | > 0.25 | Unstable, poor UX |
Common CLS Culprits in PrestaShop
- Images without dimensions - Product images, banners, thumbnails
- Web fonts loading - FOUT (Flash of Unstyled Text)
- Dynamic content injection - Promotional banners, cookie notices
- Ads and embeds - Third-party advertising, social media widgets
- Cart/wishlist updates - AJAX content loading
- Lazy-loaded modules - Blocks loading after initial render
Measuring CLS on PrestaShop
Testing Tools
1. Chrome DevTools - Performance Panel
// Run in browser console to see all layout shifts
new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (!entry.hadRecentInput) {
console.log('Layout shift:', entry);
console.log('Value:', entry.value);
console.log('Sources:', entry.sources);
}
});
}).observe({type: 'layout-shift', buffered: true});
2. Google PageSpeed Insights
- Shows CLS score for mobile and desktop
- Identifies specific shifting elements
- Provides before/after screenshots
3. Web Vitals Extension
- Real-time CLS monitoring
- Shows shifts as they happen
- Available for Chrome
4. Lighthouse in DevTools
- Detailed CLS analysis
- Screenshot timeline showing shifts
- Specific recommendations
PrestaShop-Specific CLS Issues
1. Product Images Without Dimensions
Problem: PrestaShop templates often omit width/height attributes, causing shifts when images load.
Impact: Massive CLS on category pages with multiple products.
Solution: Add Image Dimensions
Fix Product Miniatures:
{* themes/your-theme/templates/catalog/_partials/miniatures/product.tpl *}
{* BEFORE - No dimensions *}
<img src="{$product.cover.bySize.home_default.url}" alt="{$product.cover.legend}">
{* AFTER - With dimensions *}
<img
src="{$product.cover.bySize.home_default.url}"
alt="{$product.cover.legend}"
width="{$product.cover.bySize.home_default.width}"
height="{$product.cover.bySize.home_default.height}"
loading="lazy"
>
Fix Product Page Main Image:
{* themes/your-theme/templates/catalog/_partials/product-images.tpl *}
<img
src="{$product.cover.large.url}"
alt="{$product.cover.legend}"
width="{$product.cover.large.width}"
height="{$product.cover.large.height}"
itemprop="image"
>
Use Aspect Ratio Boxes:
/* Maintain aspect ratio before image loads */
.product-miniature .product-thumbnail {
position: relative;
width: 100%;
}
.product-miniature .product-thumbnail::before {
content: '';
display: block;
padding-bottom: 100%; /* 1:1 aspect ratio for square images */
}
.product-miniature .product-thumbnail img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
2. Logo and Header Elements
Problem: Site logo loads late, causing header to shift.
Solution:
A. Set Logo Dimensions in Theme:
{* themes/your-theme/templates/_partials/header.tpl *}
<a href="{$urls.base_url}">
<img
src="{$shop.logo}"
alt="{$shop.name}"
width="250"
height="80"
>
</a>
B. Preload Logo:
{* In <head> section *}
<link rel="preload" as="image" href="{$shop.logo}">
C. Reserve Space with CSS:
.header-logo {
display: block;
width: 250px;
height: 80px;
}
.header-logo img {
width: 100%;
height: 100%;
object-fit: contain;
}
3. Web Fonts Causing FOUT/FOIT
Problem: Custom fonts load late, causing text to reflow.
Solution: Optimize Font Loading
A. Use font-display: swap
/* themes/your-theme/assets/css/theme.css */
@font-face {
font-family: 'Custom Font';
src: url('../fonts/custom-font.woff2') format('woff2');
font-display: swap; /* Prevents invisible text, allows fallback */
}
B. Preload Critical Fonts:
{* In <head> section *}
<link
rel="preload"
as="font"
href="{$urls.theme_assets}fonts/custom-font.woff2"
type="font/woff2"
crossorigin
>
C. Match Fallback Font Metrics:
/* Use fallback font with similar metrics to reduce shift */
body {
font-family: 'Custom Font', Arial, sans-serif;
/* Use font-size-adjust if supported */
font-size-adjust: 0.52;
}
D. Use Font Loading API:
// Load fonts asynchronously without layout shift
if ('fonts' in document) {
Promise.all([
document.fonts.load('1em CustomFont'),
document.fonts.load('bold 1em CustomFont')
]).then(function() {
document.documentElement.classList.add('fonts-loaded');
});
}
/* Show fallback initially, switch when loaded */
body {
font-family: Arial, sans-serif;
}
.fonts-loaded body {
font-family: 'CustomFont', Arial, sans-serif;
}
4. Dynamic Promotional Banners
Problem: PrestaShop modules inject banners that push content down.
Common Culprits:
- Cookie consent banners
- Promotional top bars
- Newsletter popups
- Special offer banners
Solutions:
A. Reserve Space for Known Banners:
/* Reserve space at top for cookie banner */
body {
padding-top: 60px; /* Height of banner */
}
.cookie-banner {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 60px;
z-index: 9999;
}
/* Remove padding once banner is dismissed */
body.cookie-accepted {
padding-top: 0;
}
B. Use position: fixed/absolute for Overlays:
/* Don't let popups affect layout */
.promo-banner,
.newsletter-popup,
.cookie-notice {
position: fixed;
/* Position it without affecting document flow */
top: 0;
left: 0;
width: 100%;
z-index: 10000;
}
C. Load Banners Before Content:
Modify module hook priority to load before main content:
-- Adjust module hook position to load earlier
UPDATE ps_hook_module
SET position = 1
WHERE id_module = (SELECT id_module FROM ps_module WHERE name = 'your_banner_module')
AND id_hook = (SELECT id_hook FROM ps_hook WHERE name = 'displayTop');
5. Product Quickview / Modals
Problem: Quickview modules cause layout shift when opening.
Solution:
/* Prevent body scroll and shift when modal opens */
body.modal-open {
overflow: hidden;
/* Prevent shift from scrollbar disappearing */
padding-right: 0 !important;
}
/* Reserve space for scrollbar */
body {
overflow-y: scroll; /* Always show scrollbar */
}
6. AJAX Cart Updates
Problem: Cart block updates via AJAX, causing header shift.
Solution: Reserve Space for Cart Count:
{* themes/your-theme/templates/_partials/header.tpl *}
<div class="cart-preview" data-cart-count="{$cart.products_count}">
<span class="cart-count">{$cart.products_count}</span>
<span class="cart-total">{$cart.totals.total.value}</span>
</div>
/* Fixed width prevents shift when count changes */
.cart-count {
display: inline-block;
min-width: 20px;
text-align: center;
}
.cart-total {
display: inline-block;
min-width: 60px;
text-align: right;
}
7. Lazy-Loaded Module Content
Problem: Modules loading via AJAX push content down.
Solution:
A. Reserve Space with Skeleton Loaders:
{* themes/your-theme/modules/your_module/views/templates/hook/display.tpl *}
{if !$module_loaded}
<div class="module-skeleton" style="height: 200px;">
<div class="skeleton-loading"></div>
</div>
{else}
<div class="module-content">
{* Actual content *}
</div>
{/if}
.skeleton-loading {
width: 100%;
height: 100%;
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; }
}
B. Use min-height for Dynamic Containers:
/* Prevent collapse before content loads */
.featured-products,
.new-products,
.best-sellers {
min-height: 400px;
}
8. Responsive Images and srcset
Problem: Browser selecting different image sizes causes reflow.
Solution:
{* Provide sizes attribute to help browser choose right image *}
<img
src="{$product.cover.bySize.medium_default.url}"
srcset="
{$product.cover.bySize.small_default.url} 250w,
{$product.cover.bySize.medium_default.url} 452w,
{$product.cover.bySize.large_default.url} 800w
"
sizes="
(max-width: 576px) 250px,
(max-width: 768px) 452px,
800px
"
width="452"
height="452"
alt="{$product.cover.legend}"
>
9. Grid Layout Shifts
Problem: CSS Grid/Flexbox recalculating after images load.
Solution: Use Consistent Grid Structure:
/* Define explicit grid structure before content loads */
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
/* Prevent grid from collapsing */
grid-auto-rows: minmax(400px, auto);
}
.product-miniature {
display: flex;
flex-direction: column;
/* Ensure consistent height */
min-height: 400px;
}
PrestaShop Module-Specific Fixes
ps_imageslider (Homepage Slider)
Problem: Slider images cause major CLS on homepage.
Fix:
{* modules/ps_imageslider/views/templates/hook/slider.tpl *}
<div class="carousel-inner" role="listbox" aria-label="Carousel container">
{foreach from=$homeslider.slides item=slide}
<div class="carousel-item {if $slide@first}active{/if}">
<figure>
<img
src="{$slide.image_url}"
alt="{$slide.legend}"
width="1920"
height="600"
loading="{if $slide@first}eager{else}lazy{/if}"
>
{if $slide.description}
<figcaption class="caption">{$slide.description nofilter}</figcaption>
{/if}
</figure>
</div>
{/foreach}
</div>
/* Reserve space for slider */
.carousel-inner {
position: relative;
width: 100%;
height: 0;
padding-bottom: 31.25%; /* 16:9 aspect ratio (600/1920 * 100) */
overflow: hidden;
}
.carousel-item {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.carousel-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
ps_featuredproducts (Featured Products Block)
Problem: Products appearing causes layout shift.
Fix:
/* Reserve minimum height before products load */
#featured-products .products {
min-height: 400px;
}
/* Skeleton loader while loading */
#featured-products.loading .products::before {
content: '';
display: block;
width: 100%;
height: 400px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
ps_shoppingcart (Cart Module)
Problem: Cart preview dropdown causes header shift.
Fix:
/* Use absolute positioning for dropdown */
.cart-preview .dropdown-menu {
position: absolute;
top: 100%;
right: 0;
/* Don't affect document flow */
margin: 0;
}
/* Prevent shift when opening */
.cart-preview.show .dropdown-menu {
display: block;
}
Systematic CLS Reduction Approach
Step 1: Identify All Shifts
Use Chrome DevTools:
- Open DevTools (F12)
- Go to Performance tab
- Click Record (circle icon)
- Reload page
- Stop recording
- Look for "Experience > Layout Shift" entries
- Click each to see what shifted
Document Each Shift:
| Element | Shift Value | Cause | Priority |
|---|---|---|---|
| Product grid | 0.15 | Images without dimensions | High |
| Header | 0.05 | Logo loading late | Medium |
| Footer | 0.03 | Lazy loaded content | Low |
Step 2: Fix High-Impact Shifts First
Focus on shifts with highest impact (value > 0.05):
- Product images (category pages)
- Hero banner (homepage)
- Logo and navigation
- Dynamic promotional content
Step 3: Add Dimensions to All Images
Automated Script to Add Dimensions:
<?php
// One-time script to add dimensions to all PrestaShop images
require_once('config/config.inc.php');
$image_types = ImageType::getImagesTypes('products');
foreach ($image_types as $type) {
echo "Image Type: {$type['name']}\n";
echo "Dimensions: {$type['width']}x{$type['height']}\n";
// Update your templates to use these dimensions
}
Step 4: Test Before and After
Before Fixes:
# Run Lighthouse test
npx lighthouse https://yourstore.com --only-categories=performance --output=json --output-path=before.json
After Fixes:
# Run Lighthouse test again
npx lighthouse https://yourstore.com --only-categories=performance --output=json --output-path=after.json
# Compare results
CSS Best Practices for CLS
Use CSS Containment
/* Isolate elements to prevent shifts affecting entire page */
.product-miniature {
contain: layout style paint;
}
.featured-products {
contain: layout;
}
Avoid Animations That Shift Layout
/* BAD - Causes layout shift */
.product-miniature:hover {
margin-top: -10px;
}
/* GOOD - Uses transform (doesn't affect layout) */
.product-miniature:hover {
transform: translateY(-10px);
}
Reserve Space for Dynamic Content
/* Reserve space for elements that will appear */
.review-stars {
min-height: 20px; /* Prevent collapse if no reviews */
}
.product-flags {
min-height: 30px; /* Space for "New" or "Sale" badges */
}
JavaScript Best Practices
Avoid Inserting Content Above Viewport
// BAD - Inserts content that shifts page
document.querySelector('.header').insertAdjacentHTML('afterbegin', bannerHTML);
// GOOD - Use fixed positioning or insert below fold
var banner = document.createElement('div');
banner.className = 'promo-banner';
banner.style.position = 'fixed';
banner.innerHTML = bannerHTML;
document.body.appendChild(banner);
Set Element Dimensions Before Content Loads
// Reserve space before loading content
var container = document.querySelector('.ajax-content');
container.style.minHeight = '300px';
// Load content via AJAX
fetch('/api/content')
.then(response => response.text())
.then(html => {
container.innerHTML = html;
// Remove min-height after content loads
container.style.minHeight = '';
});
Monitoring CLS Over Time
Real User Monitoring (RUM)
// Track CLS from real users
<script type="module">
import {getCLS} from 'https://unpkg.com/web-vitals?module';
getCLS((metric) => {
// Send to analytics
gtag('event', 'CLS', {
value: Math.round(metric.value * 1000),
event_category: 'Web Vitals',
event_label: metric.id,
non_interaction: true,
});
// Also send to your logging endpoint
fetch('/api/log-cls', {
method: 'POST',
body: JSON.stringify({
cls: metric.value,
page: window.location.pathname,
timestamp: Date.now()
})
});
});
</script>
Set Up Alerts
Configure alerts for CLS degradation:
- Google Search Console (Core Web Vitals report)
- Cloudflare Web Analytics
- Custom monitoring dashboard
Quick Wins Checklist
Implement these for immediate CLS improvement:
- Add width/height to all product images
- Add width/height to logo and header elements
- Use font-display: swap for web fonts
- Preload critical fonts and images
- Reserve space for cookie banners and notices
- Use position: fixed for modals and overlays
- Set min-height for dynamic content areas
- Use CSS containment for isolated components
- Replace margin animations with transform
- Add explicit dimensions to iframes and embeds
Testing Different Device Types
Test on Real Devices:
- Mobile (phone size)
- Tablet
- Desktop
CLS varies by viewport:
- Mobile often has higher CLS
- Responsive images can cause different shifts
- Mobile menus/headers behave differently
Use Chrome DevTools Device Mode:
- Open DevTools
- Toggle device toolbar (Ctrl+Shift+M)
- Select different devices
- Test CLS on each
Next Steps
- LCP Optimization - Improve page load speed
- Troubleshooting Overview - Other PrestaShop issues
- PrestaShop Integrations - Optimize third-party scripts