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.
Banner 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>
Fix 6: Cookie Notices and Popups
Handle consent banners without shifting content.
Fixed Position Cookie Notice
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>
Related Products
<?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>