Fix CLS Issues on osCommerce (Layout Shift) | OpsBlu Docs

Fix CLS Issues on osCommerce (Layout Shift)

Stabilize osCommerce layouts by sizing catalog product images, preloading storefront fonts, and constraining infobox module containers.

Overview

Cumulative Layout Shift (CLS) measures visual stability - how much content unexpectedly shifts during page load. Poor CLS frustrates users and hurts SEO.

CLS Targets:

  • Good: < 0.1
  • Needs Improvement: 0.1 - 0.25
  • Poor: > 0.25

Check Your CLS

Method 1: PageSpeed Insights

1. Go to https://pagespeed.web.dev/
2. Enter your OSCommerce store URL
3. Check CLS score under "Diagnostics"
4. Review "Avoid large layout shifts" section

Method 2: Chrome DevTools

1. Open page in Chrome
2. F12 > Performance tab
3. Check "Experience" row
4. Record page load
5. Look for red "Layout Shift" markers
6. Click markers to see which elements shifted

Method 3: Web Vitals Extension

1. Install Web Vitals Chrome extension
2. Visit your store
3. Click extension to see real-time CLS

Common CLS Issues in OSCommerce

1. Images Without Dimensions

Problem: Images load and push content down

Bad Example:

<img src="product.jpg" alt="Product">
<!-- No width/height = layout shift when image loads -->

Good Example:

<img src="product.jpg" alt="Product" width="600" height="600">
<!-- Reserved space prevents shift -->

2. Ads and Embeds

Problem: Ad containers load and shift content

Bad Example:

<div id="ad-slot"></div>
<script>
  // Ad loads and expands, shifting content
  loadAd('ad-slot');
</script>

Good Example:

<div id="ad-slot" style="min-height: 250px;">
  <!-- Reserved space prevents shift -->
</div>

3. Web Fonts

Problem: Font swap causes text to shift

Bad Example:

@font-face {
  font-family: 'CustomFont';
  src: url('font.woff2');
  /* No font-display = invisible text then FOIT -->
}

Good Example:

@font-face {
  font-family: 'CustomFont';
  src: url('font.woff2');
  font-display: swap; /* Fallback font shows immediately */
}

4. Dynamic Content

Problem: JavaScript inserts content, shifting layout

Bad Example:

// Content loads after page renders
$.get('reviews.php', function(data) {
  $('#reviews').html(data); // Shifts content below
});

Good Example:

<div id="reviews" style="min-height: 200px;">
  Loading reviews...
</div>

Fix 1: Add Image Dimensions

Always specify width and height for images.

Update tep_image() Function

File: catalog/includes/functions/html_output.php

<?php
function tep_image($src, $alt = '', $width = '', $height = '', $parameters = '') {
  // Get image dimensions if not provided
  if (empty($width) || empty($height)) {
    $image_path = DIR_FS_CATALOG . $src;

    if (file_exists($image_path)) {
      $image_info = getimagesize($image_path);
      if ($image_info) {
        if (empty($width)) $width = $image_info[0];
        if (empty($height)) $height = $image_info[1];
      }
    }
  }

  // Build image tag with dimensions
  $image = '<img src="' . tep_output_string($src) . '" alt="' . tep_output_string($alt) . '"';

  if (!empty($width)) {
    $image .= ' width="' . tep_output_string($width) . '"';
  }

  if (!empty($height)) {
    $image .= ' height="' . tep_output_string($height) . '"';
  }

  if (!empty($parameters)) {
    $image .= ' ' . $parameters;
  }

  $image .= '>';

  return $image;
}
?>

Add Dimensions to All Images

File: catalog/product_info.php

<?php
// BEFORE - No dimensions
echo tep_image(DIR_WS_IMAGES . $product_info['products_image'], $product_info['products_name']);

// AFTER - With dimensions
echo tep_image(DIR_WS_IMAGES . $product_info['products_image'], $product_info['products_name'], 600, 600);
?>

Use aspect-ratio CSS

For responsive images:

.product-image {
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 1; /* Maintain 1:1 aspect ratio */
}
<img src="product.jpg" alt="Product" class="product-image" width="600" height="600">

Fix 2: Reserve Space for Ads

Prevent ad containers from shifting content.

File: Template files with ads

<!-- BEFORE - No reserved space -->
<div id="banner-ad"></div>

<!-- AFTER - Reserved space -->
<div id="banner-ad" style="min-height: 250px; background: #f0f0f0;">
  <div class="ad-placeholder">Advertisement</div>
</div>

<style>
.ad-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 250px;
  color: #999;
  font-size: 14px;
}
</style>

<script>
// Load ad asynchronously
loadBannerAd('banner-ad');
</script>

Tracking Scripts

File: catalog/includes/header.php

<!-- Ensure tracking scripts don't shift content -->
<script async src="gtag.js"></script>
<script async src="fbevents.js"></script>

<!-- NOT synchronous scripts that block rendering -->

Fix 3: Optimize Font Loading

Prevent font swap from shifting layout.

Use font-display: swap

File: CSS files

@font-face {
  font-family: 'CustomFont';
  src: url('fonts/customfont.woff2') format('woff2');
  font-display: swap; /* Show fallback immediately, swap when loaded */
}

body {
  font-family: 'CustomFont', Arial, sans-serif;
  /* Arial is fallback with similar metrics to prevent shift */
}

Match Fallback Font Metrics

Use fallback fonts with similar dimensions:

/* BAD - Very different metrics */
font-family: 'Fancy Font', Times New Roman;

/* GOOD - Similar metrics */
font-family: 'Open Sans', Arial, sans-serif;
font-family: 'Roboto', Helvetica, sans-serif;

Preload Critical Fonts

File: catalog/includes/header.php

<link rel="preload" href="fonts/main-font.woff2" as="font" type="font/woff2" crossorigin>

Use Font Loading API

<script>
// Load fonts with control
if ('fonts' in document) {
  document.fonts.load('1em CustomFont').then(function() {
    document.documentElement.classList.add('fonts-loaded');
  });
}
</script>

<style>
/* Before fonts load */
body {
  font-family: Arial, sans-serif;
}

/* After fonts load */
.fonts-loaded body {
  font-family: 'CustomFont', Arial, sans-serif;
}
</style>

Fix 4: Stabilize Dynamic Content

Prevent JavaScript-injected content from shifting layout.

Reserve Space for AJAX Content

File: catalog/product_info.php

<!-- Reviews loaded via AJAX -->
<div id="product-reviews" style="min-height: 300px;">
  <div class="loading">Loading reviews...</div>
</div>

<script>
$.ajax({
  url: 'product_reviews_ajax.php',
  data: {products_id: <?php echo $products_id; ?>},
  success: function(data) {
    $('#product-reviews').html(data);
    // No layout shift because space was reserved
  }
});
</script>

Use Skeleton Screens

Show placeholder content while loading:

<div id="reviews-container">
  <div class="skeleton-review">
    <div class="skeleton-avatar"></div>
    <div class="skeleton-text"></div>
    <div class="skeleton-text short"></div>
  </div>
  <div class="skeleton-review">
    <div class="skeleton-avatar"></div>
    <div class="skeleton-text"></div>
    <div class="skeleton-text short"></div>
  </div>
</div>

<style>
.skeleton-avatar {
  width: 50px;
  height: 50px;
  background: #e0e0e0;
  border-radius: 50%;
  margin-bottom: 10px;
}

.skeleton-text {
  height: 16px;
  background: #e0e0e0;
  margin-bottom: 8px;
  border-radius: 4px;
}

.skeleton-text.short {
  width: 60%;
}
</style>

<script>
// Replace skeleton with real content
loadReviews().then(function(reviews) {
  $('#reviews-container').html(reviews);
});
</script>

Animate Height Changes

If content must change height, animate smoothly:

<script>
function expandContent(element, newContent) {
  var oldHeight = element.offsetHeight;

  element.innerHTML = newContent;

  var newHeight = element.offsetHeight;

  // Animate from old to new height
  element.style.height = oldHeight + 'px';

  setTimeout(function() {
    element.style.transition = 'height 0.3s ease';
    element.style.height = newHeight + 'px';
  }, 0);
}
</script>

Fix 5: Avoid Injecting Content Above Existing Content

Don't insert content that pushes existing content down.

Bad Example

// Inserting banner at top pushes everything down
$('body').prepend('<div class="promo-banner">Sale!</div>');

Good Example

<!-- Reserve space in layout -->
<div id="promo-banner" style="min-height: 60px;"></div>

<script>
// Populate reserved space
if (shouldShowPromo()) {
  $('#promo-banner').html('<div class="promo-banner">Sale!</div>');
}
</script>

Handle consent banners without shifting content.

File: catalog/includes/footer.php

<div id="cookie-notice" style="position: fixed; bottom: 0; left: 0; right: 0; z-index: 9999;">
  <div class="cookie-content">
    We use cookies. <a href="privacy.php">Learn more</a>
    <button
  </div>
</div>

<style>
#cookie-notice {
  position: fixed; /* Doesn't affect document flow */
  bottom: 0;
  left: 0;
  right: 0;
  background: #333;
  color: #fff;
  padding: 15px;
  transform: translateY(100%);
  transition: transform 0.3s ease;
}

#cookie-notice.show {
  transform: translateY(0);
}
</style>

<script>
// Show if not accepted
if (!getCookie('cookie_consent')) {
  document.getElementById('cookie-notice').classList.add('show');
}

function acceptCookies() {
  setCookie('cookie_consent', 'accepted', 365);
  document.getElementById('cookie-notice').classList.remove('show');
}
</script>

Fix 7: Stabilize Shopping Cart Updates

Prevent cart updates from shifting layout.

File: catalog/includes/boxes/shopping_cart.php

<!-- Cart box with fixed minimum height -->
<div id="shopping-cart-box" style="min-height: 100px;">
  <?php
  if ($_SESSION['cart']->count_contents() > 0) {
    // Display cart items
  } else {
    echo 'Your cart is empty';
  }
  ?>
</div>

<script>
// Update cart via AJAX without layout shift
function updateCart() {
  $.get('ajax_cart.php', function(data) {
    $('#shopping-cart-box').html(data);
    // Height is maintained by min-height
  });
}
</script>

Fix 8: Prevent Flash of Unstyled Content (FOUC)

CSS should load before content renders.

File: catalog/includes/header.php

<head>
  <!-- Critical CSS inline -->
  <style>
    /* Inline critical above-the-fold CSS */
    body { margin: 0; font-family: Arial; }
    .header { height: 80px; background: #fff; }
    .main-content { min-height: 400px; }
  </style>

  <!-- Full stylesheet loads async -->
  <link rel="preload" href="stylesheet.css" as="style"
  <noscript><link rel="stylesheet" href="stylesheet.css"></noscript>
</head>

Fix 9: Optimize Third-Party Scripts

Third-party scripts often cause layout shifts.

Load Scripts Asynchronously

<!-- DON'T: Blocking script -->
<script src="thirdparty.js"></script>

<!-- DO: Async or defer -->
<script async src="thirdparty.js"></script>
<script defer src="thirdparty.js"></script>

Sandbox with iframe

Isolate third-party content:

<iframe src="thirdparty-widget.html"
        width="300"
        height="250"
        style="border: none;"
        loading="lazy">
</iframe>

Fix 10: Set Transform Animations Only

Animate with transform/opacity instead of layout properties.

Bad Animations (Cause Layout Shifts)

/* Triggers layout recalculation */
.element {
  transition: height 0.3s, width 0.3s, top 0.3s, left 0.3s;
}

Good Animations (No Layout Shifts)

/* Only triggers compositing */
.element {
  transition: transform 0.3s, opacity 0.3s;
}

/* Move with transform instead of top/left */
.element.moved {
  transform: translateY(100px);
}

Testing CLS Improvements

Layout Shift Recorder

Chrome DevTools method:

1. F12 > Performance
2. Click "Record"
3. Interact with page
4. Stop recording
5. Check "Experience" track for Layout Shifts
6. Click each shift to see affected elements

Measure CLS in Production

<script>
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);
    }
  }
});

observer.observe({type: 'layout-shift', buffered: true});

// Send to analytics on page unload
window.addEventListener('beforeunload', () => {
  gtag('event', 'web_vitals', {
    'event_category': 'Web Vitals',
    'event_label': 'CLS',
    'value': Math.round(clsValue * 1000),
    'metric_value': clsValue
  });
});
</script>

Quick Fixes Checklist

  • Add width/height to all images
  • Use aspect-ratio for responsive images
  • Reserve space for ads and embeds
  • Use font-display: swap
  • Preload critical fonts
  • Set min-height for dynamic content
  • Use fixed positioning for popups
  • Load third-party scripts async
  • Inline critical CSS
  • Test with Performance tab

Common OSCommerce CLS Sources

Product Image Galleries

<!-- Reserve space for image gallery -->
<div class="product-gallery" style="min-height: 600px;">
  <div class="main-image">
    <img src="product-1.jpg" width="600" height="600" alt="Product">
  </div>
  <div class="thumbnails">
    <img src="thumb-1.jpg" width="100" height="100" alt="Thumb 1">
    <img src="thumb-2.jpg" width="100" height="100" alt="Thumb 2">
    <img src="thumb-3.jpg" width="100" height="100" alt="Thumb 3">
  </div>
</div>
<?php
// File: catalog/product_info.php

// Reserve space for related products
?>
<div id="related-products" style="min-height: 300px;">
  <?php
  // Load related products
  if (count($related_products) > 0) {
    foreach ($related_products as $product) {
      echo '<div class="related-product">';
      echo tep_image(DIR_WS_IMAGES . $product['image'], $product['name'], 200, 200);
      echo '<p>' . $product['name'] . '</p>';
      echo '</div>';
    }
  }
  ?>
</div>

Advanced CLS Debugging

Identify Shifting Elements

<script>
// Log which elements cause shifts
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      console.log('Layout shift detected:');
      console.log('Value:', entry.value);
      console.log('Sources:', entry.sources);
    }
  }
});

observer.observe({type: 'layout-shift', buffered: true});
</script>

Monitor Specific Elements

<script>
// Watch for changes to specific element
const resizeObserver = new ResizeObserver(entries => {
  for (let entry of entries) {
    console.log('Element resized:', entry.target);
    console.log('New size:', entry.contentRect.width, 'x', entry.contentRect.height);
  }
});

// Monitor cart box
resizeObserver.observe(document.getElementById('shopping-cart-box'));
</script>

Next Steps

Additional Resources