PrestaShop CLS Optimization | Blue Frog Docs

PrestaShop CLS Optimization

Fix Cumulative Layout Shift (CLS) issues on PrestaShop stores by optimizing images, fonts, dynamic content, and PrestaShop-specific layout problems.

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

  1. Images without dimensions - Product images, banners, thumbnails
  2. Web fonts loading - FOUT (Flash of Unstyled Text)
  3. Dynamic content injection - Promotional banners, cookie notices
  4. Ads and embeds - Third-party advertising, social media widgets
  5. Cart/wishlist updates - AJAX content loading
  6. 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;
}

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:

  1. Open DevTools (F12)
  2. Go to Performance tab
  3. Click Record (circle icon)
  4. Reload page
  5. Stop recording
  6. Look for "Experience > Layout Shift" entries
  7. 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):

  1. Product images (category pages)
  2. Hero banner (homepage)
  3. Logo and navigation
  4. 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:

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:

  1. Open DevTools
  2. Toggle device toolbar (Ctrl+Shift+M)
  3. Select different devices
  4. Test CLS on each

Next Steps

// SYS.FOOTER