Cumulative Layout Shift (CLS) measures visual stability - how much elements move unexpectedly during page load. WooCommerce stores often experience CLS from product variations, dynamic pricing, review widgets, and images loading without dimensions.
What is CLS?
CLS is a Core Web Vital measuring unexpected layout shifts during page load.
Thresholds:
- Good: < 0.1
- Needs Improvement: 0.1 - 0.25
- Poor: > 0.25
Common CLS Causes on WooCommerce:
- Product images loading without dimensions
- Variation selection changing layout
- Dynamic price updates
- Review widgets loading late
- Add to cart buttons shifting
- Promotional banners appearing late
- Cart count badges updating
Measuring WooCommerce CLS
Google PageSpeed Insights
- Go to PageSpeed Insights
- Test WooCommerce pages:
- Product pages
- Shop/category pages
- Cart page
- View Cumulative Layout Shift score
- Check Diagnostics for specific shifting elements
Chrome DevTools
// Measure CLS in console
let cls = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
cls += entry.value;
console.log('CLS:', cls, 'Shifted element:', entry.sources);
}
}
}).observe({type: 'layout-shift', buffered: true});
Web Vitals Extension
Install Web Vitals Chrome Extension to see CLS score on every page.
Common WooCommerce CLS Issues
1. Product Images Without Dimensions
Symptoms:
- Product images loading cause layout shift
- Gallery thumbnails shift when loading
- Related products appear and shift content
Solutions:
Set Image Dimensions in HTML
// Ensure WooCommerce images have width/height attributes
add_filter('woocommerce_get_image_size_gallery_thumbnail', 'set_gallery_image_dimensions');
function set_gallery_image_dimensions($size) {
return array(
'width' => 150,
'height' => 150,
'crop' => 1,
);
}
add_filter('woocommerce_get_image_size_single', 'set_single_image_dimensions');
function set_single_image_dimensions($size) {
return array(
'width' => 800,
'height' => 800,
'crop' => 0,
);
}
Add aspect-ratio CSS
// Reserve space for product images
add_action('wp_head', 'woocommerce_image_aspect_ratio_css');
function woocommerce_image_aspect_ratio_css() {
?>
<style>
/* Reserve space for product gallery */
.woocommerce-product-gallery__wrapper {
aspect-ratio: 1 / 1;
}
/* Reserve space for thumbnails */
.woocommerce-product-gallery__image {
aspect-ratio: 1 / 1;
}
/* Shop page product images */
.woocommerce-loop-product__link img {
aspect-ratio: 4 / 3;
width: 100%;
height: auto;
}
</style>
<?php
}
Use Explicit Width/Height Attributes
// Add width/height to product images
add_filter('wp_get_attachment_image_attributes', 'add_woocommerce_image_dimensions', 10, 3);
function add_woocommerce_image_dimensions($attr, $attachment, $size) {
// Only for WooCommerce images
if (is_woocommerce() || is_product()) {
$image_meta = wp_get_attachment_metadata($attachment->ID);
if (!empty($image_meta['width']) && !empty($image_meta['height'])) {
$attr['width'] = $image_meta['width'];
$attr['height'] = $image_meta['height'];
}
}
return $attr;
}
2. Product Variation Selection Causing Shifts
Symptoms:
- Price changes size when variation selected
- Product description changes height
- Image gallery shifts
- Add to cart button moves
Solutions:
Reserve Space for Variable Prices
add_action('wp_head', 'reserve_space_for_variation_price');
function reserve_space_for_variation_price() {
if (!is_product()) return;
global $product;
if (!$product || !$product->is_type('variable')) return;
?>
<style>
/* Reserve space for price variations */
.product .price {
min-height: 40px; /* Adjust based on your theme */
display: flex;
align-items: center;
}
/* Prevent description height changes */
.woocommerce-variation-description {
min-height: 60px;
}
/* Stable add to cart button */
.single_add_to_cart_button {
min-width: 200px;
min-height: 48px;
}
</style>
<?php
}
Prevent Image Gallery Shifts
add_action('wp_head', 'stabilize_variation_gallery');
function stabilize_variation_gallery() {
?>
<style>
/* Prevent gallery shift on variation change */
.woocommerce-product-gallery {
min-height: 600px; /* Set to your gallery height */
}
.woocommerce-product-gallery__wrapper {
position: relative;
}
.woocommerce-product-gallery__image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
transition: opacity 0.3s;
}
.woocommerce-product-gallery__image.active {
position: relative;
z-index: 1;
}
</style>
<?php
}
Preload Variation Images
// Preload variation images to prevent shift
add_action('woocommerce_after_add_to_cart_form', 'preload_variation_images');
function preload_variation_images() {
global $product;
if (!$product->is_type('variable')) return;
$variations = $product->get_available_variations();
$preload_count = 0;
foreach ($variations as $variation) {
if ($preload_count >= 3) break; // Limit to first 3 variations
if (!empty($variation['image']['url'])) {
echo '<link rel="preload" as="image" href="' . esc_url($variation['image']['url']) . '">';
$preload_count++;
}
}
}
3. Dynamic Price Updates
Symptoms:
- Sale price badge appears late
- Price strikethrough shifts layout
- Currency symbol changes size
Solutions:
// Reserve space for sale badges
add_action('wp_head', 'reserve_space_for_sale_badge');
function reserve_space_for_sale_badge() {
?>
<style>
/* Reserve space for "On Sale" badge */
.onsale {
position: absolute !important;
top: 0;
right: 0;
min-width: 60px;
min-height: 60px;
}
/* Stable price display */
.product .price {
min-width: 100px;
display: inline-block;
}
/* Regular/sale price alignment */
.product .price del {
display: inline-block;
min-width: 80px;
}
</style>
<?php
}
4. Product Reviews Widget Loading
Symptoms:
- Reviews appear late and push content down
- Star ratings shift when loading
- Review count changes layout
Solutions:
Reserve Space for Reviews
add_action('wp_head', 'reserve_space_for_reviews');
function reserve_space_for_reviews() {
if (!is_product()) return;
?>
<style>
/* Reserve minimum space for reviews section */
#reviews {
min-height: 400px;
}
.woocommerce-Reviews {
min-height: 300px;
}
/* Stable star rating display */
.star-rating {
display: inline-block;
width: 5.4em;
height: 1.1em;
position: relative;
}
/* Review list stable height */
.commentlist {
min-height: 200px;
}
</style>
<?php
}
Lazy Load Reviews Below Fold
// Only load reviews when scrolled to
add_action('wp_footer', 'lazy_load_woocommerce_reviews');
function lazy_load_woocommerce_reviews() {
if (!is_product()) return;
?>
<script>
// Lazy load reviews when visible
if ('IntersectionObserver' in window) {
const reviewsObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Reviews now visible, ensure they don't shift
entry.target.style.minHeight = entry.target.offsetHeight + 'px';
}
});
});
const reviewsSection = document.querySelector('#reviews');
if (reviewsSection) {
reviewsObserver.observe(reviewsSection);
}
}
</script>
<?php
}
5. Add to Cart Button Shifts
Symptoms:
- Button moves when quantity selector loads
- Button shifts when out of stock message appears
- Variable product button changes size
Solutions:
add_action('wp_head', 'stabilize_add_to_cart_button');
function stabilize_add_to_cart_button() {
?>
<style>
/* Stable add to cart area */
.cart {
min-height: 80px;
display: flex;
align-items: center;
gap: 10px;
}
/* Quantity selector stable width */
.quantity {
min-width: 100px;
}
/* Button stable dimensions */
.single_add_to_cart_button {
min-width: 180px;
min-height: 48px;
white-space: nowrap;
}
/* Out of stock message */
.stock.out-of-stock {
min-height: 48px;
display: flex;
align-items: center;
}
/* Variable product form */
.variations_form .variations {
min-height: 100px;
}
</style>
<?php
}
6. Cart Count Badge Updates
Symptoms:
- Header cart count appears/updates causing shift
- Mini cart widget expands unexpectedly
- Cart total updates and moves elements
Solutions:
add_action('wp_head', 'stabilize_cart_widget');
function stabilize_cart_widget() {
?>
<style>
/* Stable cart count badge */
.cart-count,
.cart-contents-count {
min-width: 20px;
min-height: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* Mini cart stable dimensions */
.widget_shopping_cart {
min-width: 300px;
}
.widget_shopping_cart_content {
min-height: 150px;
}
/* Cart total */
.woocommerce-mini-cart__total {
min-height: 40px;
}
</style>
<?php
}
7. Promotional Banners and Notices
Symptoms:
- Sale banners appear and push content
- Stock notices shift layout
- Coupon messages appear late
Solutions:
// Reserve space for WooCommerce notices
add_action('wp_head', 'reserve_space_for_notices');
function reserve_space_for_notices() {
?>
<style>
/* Reserve space for notice area */
.woocommerce-notices-wrapper {
min-height: 60px;
}
/* Individual notice stable height */
.woocommerce-message,
.woocommerce-error,
.woocommerce-info {
min-height: 50px;
display: flex;
align-items: center;
}
/* Sale flash badge - absolute positioning to avoid shifts */
.onsale {
position: absolute !important;
top: 10px;
left: 10px;
z-index: 10;
}
</style>
<?php
}
WooCommerce-Specific CLS Optimizations
Optimize Product Grid Layout
add_action('wp_head', 'optimize_product_grid_cls');
function optimize_product_grid_cls() {
?>
<style>
/* Use CSS Grid with stable rows */
ul.products {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-auto-rows: 400px; /* Set fixed row height */
gap: 20px;
}
/* Product card stable dimensions */
.product {
min-height: 380px;
display: flex;
flex-direction: column;
}
/* Image container stable */
.product .woocommerce-loop-product__link {
flex: 0 0 250px; /* Fixed height for image */
}
/* Product info area */
.product .woocommerce-loop-product__title,
.product .price,
.product .button {
flex: 0 0 auto;
}
</style>
<?php
}
Optimize Checkout Page
add_action('wp_head', 'optimize_checkout_cls');
function optimize_checkout_cls() {
if (!is_checkout()) return;
?>
<style>
/* Stable checkout form */
.woocommerce-checkout {
min-height: 800px;
}
/* Billing/shipping fields stable */
.woocommerce-billing-fields,
.woocommerce-shipping-fields {
min-height: 500px;
}
/* Order review stable */
#order_review {
min-height: 300px;
}
/* Payment methods stable */
#payment {
min-height: 200px;
}
/* Each payment method option */
.wc_payment_method {
min-height: 60px;
}
</style>
<?php
}
Prevent Font Loading Shifts
// Preload fonts to prevent layout shift
add_action('wp_head', 'preload_woocommerce_fonts', 1);
function preload_woocommerce_fonts() {
?>
<link rel="preload" href="<?php echo get_stylesheet_directory_uri(); ?>/fonts/your-font.woff2" as="font" type="font/woff2" crossorigin>
<style>
/* Use font-display: swap to prevent invisible text */
@font-face {
font-family: 'YourFont';
src: url('fonts/your-font.woff2') format('woff2');
font-display: swap;
}
</style>
<?php
}
Testing CLS Improvements
Measure CLS in DevTools
- Open Chrome DevTools
- Go to Performance tab
- Enable Web Vitals in settings
- Record page load
- Check Experience section for layout shifts
- Identify shifting elements
Use Layout Shift Debugger
// Highlight elements causing layout shifts
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput && entry.value > 0.001) {
entry.sources.forEach(source => {
source.node.style.outline = '3px solid red';
console.log('Layout shift detected:', source.node, 'Score:', entry.value);
});
}
}
}).observe({type: 'layout-shift', buffered: true});
PageSpeed Insights Field Data
Check real-user CLS scores:
- Enter your WooCommerce URL
- View Field Data (75th percentile)
- Compare product pages vs shop pages
- Monitor over time
Common Mistakes to Avoid
- No image dimensions - Always set width/height on images
- Dynamic content without reserved space - Reserve space for variations, reviews, notices
- Absolute positioning without container - Wrap absolutely positioned elements
- Font loading without optimization - Use font-display: swap
- Late-loading widgets - Reserve space or lazy load properly
- Animations during load - Avoid transforms/animations on page load
WooCommerce Plugin Conflicts
Some plugins cause CLS issues:
Common Culprits:
- Variation swatches - Images loading without dimensions
- Quick view plugins - Modals shifting content
- Recently viewed products - Widget appearing late
- Countdown timers - Dynamic content shifting
Solution: Test with plugins disabled, identify culprit, find alternative or configure properly.
Best Practices
- Set explicit dimensions on all images, videos, embeds
- Reserve space for dynamic content (variations, reviews, notices)
- Use CSS Grid/Flexbox with stable dimensions
- Preload critical resources (fonts, hero images)
- Avoid injecting content above existing content
- Test on real devices - Mobile often has worse CLS
- Monitor field data - Real users matter more than lab tests
Next Steps
- LCP Troubleshooting - Optimize page load speed
- Tracking Issues - Ensure tracking works with layout fixes
- Web Vitals Guide