WordPress CLS (Cumulative Layout Shift) Troubleshooting | Blue Frog Docs

WordPress CLS (Cumulative Layout Shift) Troubleshooting

Fix layout shift issues on WordPress caused by themes, ads, fonts, images, and dynamic content

WordPress CLS (Cumulative Layout Shift) Troubleshooting

Cumulative Layout Shift (CLS) measures visual stability. WordPress sites often suffer from CLS due to dynamic ad insertion, web fonts, theme layouts, and plugin-injected content. This guide provides WordPress-specific solutions.

What is CLS?

CLS is a Core Web Vital that measures unexpected layout shifts during page load.

Thresholds:

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

Common CLS Causes on WordPress:

  • Images without width/height attributes
  • Web fonts loading (FOUT/FOIT)
  • Dynamic ad insertion
  • Cookie consent banners
  • Lazy-loaded images
  • Embeds (YouTube, Twitter, Instagram)

Measuring WordPress CLS

Google PageSpeed Insights

  1. Go to PageSpeed Insights
  2. Enter your WordPress URL
  3. View Cumulative Layout Shift metric
  4. Check Diagnostics → Avoid large layout shifts for specific elements

Chrome DevTools

Layout Shift Regions (Visual):

  1. Open DevTools → Performance tab
  2. Check Experience checkbox
  3. Click Record and reload page
  4. Look for red "Layout Shift" markers
  5. Click marker to see which element shifted

Console Logging:

// Log all layout shifts
let cls = 0;
new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
            cls += entry.value;
            console.log('Layout Shift:', entry.value, 'Total CLS:', cls);
            console.log('Shifted elements:', entry.sources);
        }
    }
}).observe({type: 'layout-shift', buffered: true});

Web Vitals Chrome Extension

Install Web Vitals to see CLS on every WordPress page.

Common WordPress CLS Issues

1. Images Without Dimensions

Symptoms:

  • Content jumps down as images load
  • Blog post images cause shifts
  • WooCommerce product images shift layout

Solution: Always Set Width & Height

WordPress 5.5+ automatically adds width/height to images, but check:

// Ensure WordPress adds width/height attributes
add_filter('wp_img_tag_add_width_and_height_attr', '__return_true');

For existing images without dimensions:

// Automatically add dimensions to images in content
add_filter('the_content', 'add_img_dimensions');
function add_img_dimensions($content) {
    // Match img tags without width/height
    preg_match_all('/<img[^>]+>/', $content, $images);

    foreach ($images[0] as $image) {
        // Skip if already has width/height
        if (strpos($image, 'width=') !== false || strpos($image, 'height=') !== false) {
            continue;
        }

        // Extract src
        preg_match('/src="([^"]+)"/', $image, $src);
        if (!isset($src[1])) {
            continue;
        }

        // Get image dimensions
        $attachment_id = attachment_url_to_postid($src[1]);
        if ($attachment_id) {
            $image_meta = wp_get_attachment_metadata($attachment_id);
            if ($image_meta) {
                $new_image = str_replace('<img', '<img width="' . $image_meta['width'] . '" height="' . $image_meta['height'] . '"', $image);
                $content = str_replace($image, $new_image, $content);
            }
        }
    }

    return $content;
}

CSS aspect-ratio (modern solution):

/* In your theme CSS */
img {
    max-width: 100%;
    height: auto;
    aspect-ratio: attr(width) / attr(height); /* Prevents shift even before load */
}

Or use explicit aspect-ratio:

.featured-image {
    aspect-ratio: 16 / 9; /* For 16:9 images */
    width: 100%;
    height: auto;
}

2. Lazy Loading Images

Symptoms:

  • Images above the fold cause layout shifts
  • Lazy load placeholder → full image causes jump

Solutions:

Don't Lazy Load Above-the-Fold Images

// Disable lazy loading for featured images (often above fold)
add_filter('wp_lazy_loading_enabled', 'disable_lazy_load_featured', 10, 2);
function disable_lazy_load_featured($default, $tag_name) {
    if ('img' === $tag_name && is_singular() && has_post_thumbnail()) {
        return false;
    }
    return $default;
}

Or manually control:

<!-- Above the fold: eager loading -->
<img src="hero.jpg" width="1200" height="600" loading="eager" alt="Hero">

<!-- Below the fold: lazy loading -->
<img src="content.jpg" width="800" height="400" loading="lazy" alt="Content">

Use Placeholder with Same Dimensions

If using lazy load plugin (Lazy Load by WP Rocket, etc.):

// Ensure placeholder has same dimensions as final image
add_filter('lazyload_placeholder_image', 'custom_lazy_placeholder');
function custom_lazy_placeholder($placeholder) {
    // Use a tiny transparent GIF with same aspect ratio
    return 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"%3E%3C/svg%3E';
}

3. Web Fonts Loading (FOUT/FOIT)

Symptoms:

  • Text appears, then changes font (Flash of Unstyled Text)
  • Text invisible, then appears (Flash of Invisible Text)
  • Headers/body text jumps when font loads

Solutions:

Use font-display: swap

Fastest solution - show fallback font immediately:

@font-face {
    font-family: 'Your Custom Font';
    src: url('your-font.woff2') format('woff2');
    font-display: swap; /* Show fallback immediately, swap when loaded */
}

For Google Fonts:

<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">

Preload Critical Fonts

add_action('wp_head', 'preload_fonts', 1);
function preload_fonts() {
    ?>
    <link rel="preload" href="<?php echo get_stylesheet_directory_uri(); ?>/fonts/primary-font.woff2" as="font" type="font/woff2" crossorigin>
    <?php
}

Match Fallback Font Metrics

Reduce layout shift by matching fallback font size to web font:

body {
    font-family: 'Your Web Font', Arial, sans-serif;
    /* Adjust line-height and letter-spacing to match web font */
    line-height: 1.5;
    letter-spacing: 0.02em;
}

Or use Fallback Font Generator to create perfect fallback.

Use System Fonts (No CLS)

Eliminate web fonts entirely:

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
}

4. Dynamic Ad Insertion

Symptoms:

  • Content jumps as ads load
  • Sidebar ads push content down
  • In-content ads shift paragraphs

Solutions:

Reserve Ad Space with Placeholder

/* Reserve exact space for ads */
.ad-container {
    min-height: 250px; /* Match ad unit height */
    min-width: 300px; /* Match ad unit width */
    background: #f5f5f5; /* Placeholder color */
}

For responsive ads:

.ad-container {
    aspect-ratio: 300 / 250; /* Common ad size */
    width: 100%;
    max-width: 300px;
}

Use AdSense Auto Ads Carefully

Google AdSense Auto Ads can cause CLS. Solutions:

  1. Disable Auto Ads, use manually placed units instead
  2. Reserve space for common Auto Ad placements
  3. Use AdSense Anchor Ads only (sticky ads, less CLS)

Plugin: Ad Inserter allows precise ad placement with reserved space.

Symptoms:

  • Banner pushes content down on first visit
  • Banner appears after page loads

Solutions:

Position Banner as Overlay

Don't push content - overlay instead:

.cookie-banner {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 9999;
    /* This doesn't push content, no CLS */
}

Load Banner Immediately in <head>

add_action('wp_head', 'inline_cookie_banner_css', 1);
function inline_cookie_banner_css() {
    ?>
    <style>
        .cookie-banner {
            position: fixed;
            bottom: 0;
            width: 100%;
            background: #333;
            color: #fff;
            padding: 15px;
            text-align: center;
            z-index: 9999;
            transform: translateY(100%); /* Hidden initially */
        }
        .cookie-banner.show {
            transform: translateY(0); /* Slide up without CLS */
        }
    </style>
    <?php
}

Popular consent plugins that minimize CLS:

  • CookieYes - Overlay-based
  • Complianz - Pre-rendered banner
  • Cookie Notice - Simple, overlay

6. WordPress Theme Layout Issues

Symptoms:

  • Header height changes after load
  • Sidebar jumps
  • Navigation menu shifts

Solutions:

Set Explicit Header Height

.site-header {
    height: 80px; /* Fixed height */
}

.site-logo img {
    height: 60px; /* Fixed logo height */
    width: auto;
}

Reserve Sidebar Space

.sidebar {
    min-height: 500px; /* Prevent collapsing */
}

.widget {
    margin-bottom: 30px;
}

Fix Navigation Height

.main-navigation {
    height: 60px;
    line-height: 60px;
}

.main-navigation ul {
    margin: 0;
    padding: 0;
}

7. Embeds (YouTube, Twitter, Instagram)

Symptoms:

  • Video embeds cause layout jump
  • Social media embeds shift content

Solutions:

Use Aspect Ratio Containers

For YouTube embeds:

<div style="position: relative; padding-bottom: 56.25%; height: 0;">
    <iframe
        src="https://www.youtube.com/embed/VIDEO_ID"
        style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"
        frameborder="0"
        allowfullscreen>
    </iframe>
</div>

Or use CSS:

.video-container {
    position: relative;
    padding-bottom: 56.25%; /* 16:9 aspect ratio */
    height: 0;
    overflow: hidden;
}

.video-container iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

WordPress Block Editor (Gutenberg)

Gutenberg automatically wraps embeds, but verify:

<!-- WordPress should output this -->
<div class="wp-block-embed__wrapper">
    <iframe...></iframe>
</div>

If not, add wrapper manually in theme:

add_filter('embed_oembed_html', 'wrap_embed_with_aspect_ratio', 10, 3);
function wrap_embed_with_aspect_ratio($html, $url, $attr) {
    if (strpos($url, 'youtube.com') !== false || strpos($url, 'vimeo.com') !== false) {
        return '<div class="video-container">' . $html . '</div>';
    }
    return $html;
}

8. Infinite Scroll / AJAX Loading

Symptoms:

  • Content loads below, pushes existing content up
  • Footer jumps as more posts load

Solutions:

Reserve Space for Loaded Content

// When loading more posts via AJAX
jQuery.ajax({
    url: ajaxurl,
    beforeSend: function() {
        // Add placeholder with estimated height
        jQuery('.posts-container').append('<div class="loading-placeholder" style="height: 500px; background: #f5f5f5;"></div>');
    },
    success: function(data) {
        // Remove placeholder, add real content
        jQuery('.loading-placeholder').remove();
        jQuery('.posts-container').append(data);
    }
});

Use Pagination Instead

Infinite scroll inherently causes CLS. Consider traditional pagination for better CLS.

9. WordPress Plugins Causing CLS

Common Culprits:

Sliders (Revolution Slider, LayerSlider, etc.)

  • Load slowly, cause header shifts
  • Solution: Use CSS-only sliders, or reserve exact slider height
.slider-container {
    height: 500px; /* Fixed height */
    overflow: hidden;
}
  • Load after content, push footer
  • Solution: Load related posts server-side (not AJAX), or reserve space

Social Sharing Buttons

  • Load asynchronously, shift content
  • Solution: Use static SVG icons instead of widget scripts
// Remove social widget scripts that cause CLS
add_action('wp_enqueue_scripts', 'remove_social_scripts', 100);
function remove_social_scripts() {
    wp_dequeue_script('addthis-widget');
    wp_dequeue_script('sharethis-widget');
}

10. WooCommerce CLS Issues

Symptoms:

  • Product images shift on product pages
  • Cart updates cause layout jumps
  • Checkout page shifts as fields load

Solutions:

Set WooCommerce Image Dimensions

add_action('after_setup_theme', 'woocommerce_image_dimensions');
function woocommerce_image_dimensions() {
    add_theme_support('woocommerce', array(
        'thumbnail_image_width' => 300,
        'single_image_width' => 600,
        'product_grid' => array(
            'default_rows' => 3,
            'min_rows' => 1,
            'max_rows' => 8,
            'default_columns' => 4,
            'min_columns' => 1,
            'max_columns' => 6
        )
    ));
}
.woocommerce-product-gallery {
    min-height: 500px; /* Prevents shift while gallery loads */
}

.woocommerce-product-gallery__image {
    aspect-ratio: 1 / 1; /* Square product images */
}

Fix Cart Update Shifts

.woocommerce-cart-form {
    min-height: 300px; /* Prevent collapse during AJAX update */
}

Advanced WordPress CLS Fixes

Eliminate Render-Blocking CSS

// Inline critical CSS, defer the rest
add_action('wp_head', 'inline_critical_css', 1);
function inline_critical_css() {
    ?>
    <style>
        /* Critical CSS for above-the-fold content */
        body { margin: 0; font-family: sans-serif; }
        .site-header { height: 80px; background: #fff; }
        /* ... */
    </style>
    <?php
}

// Defer non-critical CSS
add_action('wp_enqueue_scripts', 'defer_non_critical_css');
function defer_non_critical_css() {
    wp_dequeue_style('theme-style');
    add_action('wp_footer', function() {
        echo '<link rel="stylesheet" href="' . get_stylesheet_uri() . '" media="print" onload="this.media=\'all\'">';
    });
}

Preload Key Resources

add_action('wp_head', 'preload_cls_critical_resources', 1);
function preload_cls_critical_resources() {
    ?>
    <!-- Preload fonts -->
    <link rel="preload" href="<?php echo get_stylesheet_directory_uri(); ?>/fonts/primary.woff2" as="font" type="font/woff2" crossorigin>

    <!-- Preload hero image -->
    <?php if (is_front_page() && has_post_thumbnail()) : ?>
        <link rel="preload" as="image" href="<?php echo get_the_post_thumbnail_url(null, 'full'); ?>">
    <?php endif; ?>
    <?php
}

Testing CLS Improvements

Before/After Testing

  1. Baseline:

    • Run PageSpeed Insights
    • Record CLS score
    • Note which elements shift
  2. Make Changes

  3. Verify:

    • Clear all caches
    • Re-run PageSpeed Insights
    • Use Chrome DevTools Performance to visualize shifts

Real User Monitoring

Install Google Analytics 4 Web Vitals tracking:

add_action('wp_footer', 'track_web_vitals');
function track_web_vitals() {
    ?>
    <script>
        // Track CLS with GA4
        new PerformanceObserver((list) => {
            for (const entry of list.getEntries()) {
                if (!entry.hadRecentInput) {
                    gtag('event', 'web_vitals', {
                        metric_name: 'CLS',
                        metric_value: entry.value,
                        metric_id: entry.id
                    });
                }
            }
        }).observe({type: 'layout-shift', buffered: true});
    </script>
    <?php
}

WordPress CLS Checklist

  • All images have width & height attributes
  • Above-the-fold images not lazy loaded
  • Web fonts use font-display: swap
  • Critical fonts preloaded
  • Ad containers have reserved space
  • Cookie banner uses fixed/overlay positioning
  • Header has explicit height
  • Embeds wrapped in aspect-ratio containers
  • WooCommerce images have fixed dimensions
  • No render-blocking CSS above the fold
  • Slider containers have fixed height
  • Sidebar has min-height
  • Social widgets use static icons (not async scripts)
  • Test with Chrome DevTools Performance tab

WordPress Plugins That Help CLS

Performance Plugins:

  • WP Rocket - Defers CSS/JS, lazy loading with CLS prevention
  • Perfmatters - Disables unused features causing CLS
  • Asset CleanUp - Controls script loading to prevent shifts

Image Optimization:

  • ShortPixel - Automatic width/height, WebP
  • Imagify - Dimension preservation, lazy load config

CLS-Specific:

  • Flying Scripts - Delays scripts until interaction (reduces CLS from async scripts)

Next Steps

// SYS.FOOTER