Sitecore CLS Optimization | Blue Frog Docs

Sitecore CLS Optimization

Fix Cumulative Layout Shift issues in Sitecore through proper image sizing, component rendering, and personalization optimization

Sitecore CLS Optimization

Learn how to eliminate Cumulative Layout Shift (CLS) issues in Sitecore websites through proper image dimensions, component optimization, and personalization best practices.

Understanding CLS in Sitecore

Cumulative Layout Shift measures visual stability. In Sitecore, CLS is often caused by:

  • Images without dimensions from Media Library
  • Late-loading personalized components
  • Experience Editor placeholder shifts
  • Dynamic rendering variants
  • Lazy-loaded Sitecore components
  • Web fonts loading late

CLS Targets

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

Identify CLS Issues

Using Chrome DevTools

  1. Open Chrome DevTools (F12)
  2. Go to Performance tab
  3. Check Experience section
  4. Look for Layout Shift events (red bars)
  5. Click on shifts to see affected elements

Layout Shift Regions

Enable layout shift regions in DevTools:

  1. Press Cmd/Ctrl + Shift + P
  2. Type "Rendering"
  3. Select "Show Rendering"
  4. Check "Layout Shift Regions"

Shifted elements will flash blue when layout shifts occur.

1. Image Dimension Fixes

Always Specify Dimensions in Razor

Bad (Causes CLS):

@{
    var imageItem = (Sitecore.Data.Items.MediaItem)Sitecore.Context.Database.GetItem(Model.Item["Image"]);
    var imageUrl = Sitecore.Resources.Media.MediaManager.GetMediaUrl(imageItem);
}

<img src="@imageUrl" alt="@imageItem.Alt" />

Good (Prevents CLS):

@using Sitecore.Resources.Media

@{
    var imageItem = (Sitecore.Data.Items.MediaItem)Sitecore.Context.Database.GetItem(Model.Item["Image"]);

    if (imageItem != null)
    {
        // Get image dimensions
        var width = imageItem.InnerItem["Width"];
        var height = imageItem.InnerItem["Height"];

        var mediaOptions = new MediaUrlBuilderOptions
        {
            Width = 1200,
            Height = 675
        };

        var imageUrl = MediaManager.GetMediaUrl(imageItem, mediaOptions);

        <img src="@HashingUtils.ProtectAssetUrl(imageUrl)"
             alt="@imageItem.Alt"
             width="@width"
             height="@height" />
    }
}

Helper Method for Responsive Images

// /Helpers/ImageHelper.cs
using Sitecore.Data.Items;
using Sitecore.Resources.Media;

namespace YourProject.Helpers
{
    public static class ImageHelper
    {
        public static string GetResponsiveImageHtml(MediaItem imageItem, int maxWidth, int maxHeight, string cssClass = "")
        {
            if (imageItem == null) return string.Empty;

            var aspectRatio = CalculateAspectRatio(imageItem);

            var html = $@"
                <img src='{MediaManager.GetMediaUrl(imageItem, new MediaUrlBuilderOptions { Width = maxWidth, Height = maxHeight })}'
                     alt='{imageItem.Alt}'
                     width='{maxWidth}'
                     height='{maxHeight}'
                     class='{cssClass}'
                     style='aspect-ratio: {aspectRatio};' />";

            return html;
        }

        private static string CalculateAspectRatio(MediaItem imageItem)
        {
            var width = int.TryParse(imageItem.InnerItem["Width"], out var w) ? w : 1;
            var height = int.TryParse(imageItem.InnerItem["Height"], out var h) ? h : 1;

            return $"{width} / {height}";
        }
    }
}

Use in Razor:

@using YourProject.Helpers

@Html.Raw(ImageHelper.GetResponsiveImageHtml(imageItem, 1200, 675, "hero-image"))

CSS Aspect Ratio

Use aspect-ratio CSS property:

@{
    var imageItem = (MediaItem)Sitecore.Context.Database.GetItem(Model.Item["Image"]);
    var width = imageItem.InnerItem["Width"];
    var height = imageItem.InnerItem["Height"];
    var aspectRatio = $"{width} / {height}";
}

<img src="@imageUrl"
     alt="@imageItem.Alt"
     style="aspect-ratio: @aspectRatio; width: 100%; height: auto;" />

2. Sitecore Component Rendering

Reserve Space for Components

Prevent layout shift from lazy-loaded components:

@* Component with reserved space *@
<div class="component-wrapper" style="min-height: 400px;">
    @Html.Sitecore().Placeholder("dynamic-content")
</div>

Fixed Height Containers

@* For components with known dimensions *@
<div class="news-feed" style="height: 600px; overflow-y: auto;">
    @Html.Sitecore().Placeholder("news-items")
</div>

Skeleton Loading

Show skeleton while components load:

@* Skeleton loader *@
<div class="skeleton-wrapper">
    <div class="skeleton-item" style="height: 200px; background: #f0f0f0;"></div>
</div>

<style>
    .skeleton-item {
        animation: pulse 1.5s ease-in-out infinite;
    }

    @@keyframes pulse {
        0%, 100% { opacity: 1; }
        50% { opacity: 0.5; }
    }
</style>

3. Personalization CLS Fixes

Pre-Allocate Personalized Content Space

Bad (Causes CLS):

@* Default content loads, then personalized content replaces it *@
<div>
    @Html.Sitecore().Placeholder("personalized-banner")
</div>

Good (Prevents CLS):

@* Reserve space matching largest variant *@
<div style="min-height: 300px;">
    @Html.Sitecore().Placeholder("personalized-banner")
</div>

Server-Side Personalization

Use server-side personalization instead of client-side when possible:

// Controller rendering with personalization
public ActionResult PersonalizedComponent()
{
    var rendering = Sitecore.Mvc.Presentation.RenderingContext.Current.Rendering;
    var personalizedContent = GetPersonalizedContent(rendering);

    return View(personalizedContent);
}

Consistent Component Heights

Ensure all personalization variants have similar dimensions:

@* All variants should have consistent height *@
<div class="banner-variant" style="min-height: 250px;">
    @if (Model.VariantType == "A")
    {
        <div class="variant-a">Content A</div>
    }
    else
    {
        <div class="variant-b">Content B</div>
    }
</div>

<style>
    .variant-a, .variant-b {
        min-height: 250px;
    }
</style>

4. Experience Editor Considerations

Placeholder Dimensions

Set fixed dimensions for Experience Editor placeholders:

@if (Sitecore.Context.PageMode.IsExperienceEditorEditing)
{
    <div class="scEmptyPlaceholder" style="min-height: 200px; border: 1px dashed #ccc;">
        @Html.Sitecore().Placeholder("content")
    </div>
}
else
{
    <div>
        @Html.Sitecore().Placeholder("content")
    </div>
}

Preview Mode Testing

Test in Preview mode (not just Experience Editor):

@if (Sitecore.Context.PageMode.IsNormal || Sitecore.Context.PageMode.IsPreview)
{
    // Production rendering
}

5. Web Font Optimization

Preload Critical Fonts

@* In <head> *@
<link rel="preload" href="/fonts/your-font.woff2" as="font" type="font/woff2" crossorigin>

Font Display Strategy

/* CSS */
@font-face {
    font-family: 'YourFont';
    src: url('/fonts/your-font.woff2') format('woff2');
    font-display: swap; /* or 'optional' for less CLS */
}

System Font Stack Fallback

body {
    font-family: 'YourFont', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif;
}

6. SXA-Specific CLS Fixes

SXA Component Spacing

@using Sitecore.XA.Foundation.Mvc.Extensions

@* Ensure consistent spacing for SXA components *@
<div class="component component-content" style="min-height: 150px;">
    @Html.Sxa().Component(Model.RenderingItem.Name, new { CssClass = "fixed-height" })
</div>

SXA Grid Stability

@* Use SXA grid with fixed dimensions *@
<div class="row component-spacing" style="min-height: 300px;">
    @Html.Sxa().Placeholder("row-1-1")
</div>

7. Dynamic Content Loading

Intersection Observer for Lazy Loading

// /scripts/lazy-loading.js
document.addEventListener('DOMContentLoaded', function() {
    const lazyComponents = document.querySelectorAll('[data-lazy-component]');

    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const component = entry.target;
                const componentUrl = component.dataset.lazyComponent;

                // Reserve space before loading
                const placeholder = document.createElement('div');
                placeholder.style.minHeight = component.dataset.minHeight || '200px';
                component.appendChild(placeholder);

                // Load component
                fetch(componentUrl)
                    .then(response => response.text())
                    .then(html => {
                        component.innerHTML = html;
                    });

                observer.unobserve(component);
            }
        });
    }, { rootMargin: '50px' });

    lazyComponents.forEach(component => observer.observe(component));
});

Use in Razor:

<div data-lazy-component="/api/component/load"
     data-min-height="300px"
     style="min-height: 300px;">
    <!-- Component loads here -->
</div>

Fixed Height Carousels

@* Prevent CLS from carousel height changes *@
<div class="carousel-container" style="height: 400px;">
    @foreach (var slide in Model.Slides)
    {
        <div class="slide" style="height: 400px;">
            @Html.Sitecore().Field("Image", slide)
        </div>
    }
</div>

Slick Slider Example

// Initialize slider with fixed height
$('.slider').slick({
    adaptiveHeight: false, // Prevent height changes
    lazyLoad: 'progressive'
});
.slider {
    height: 500px; /* Fixed height */
}

.slider .slide {
    height: 500px;
}

9. Banner and Ad Space

Reserve Ad Space

@* Reserve space for ads *@
<div class="ad-container" style="min-height: 250px; min-width: 300px;">
    @Html.Sitecore().Placeholder("advertisement")
</div>

Prevent Banner CLS

@* Fixed dimensions for banner *@
<div class="banner-wrapper" style="aspect-ratio: 16 / 9; max-width: 100%;">
    <img src="@bannerUrl" alt="Banner" style="width: 100%; height: auto;" />
</div>

10. Form Field Spacing

Sitecore Forms CLS Prevention

/* Reserve space for form fields */
.sc-form-field {
    min-height: 60px;
    margin-bottom: 1rem;
}

/* Reserve space for validation messages */
.sc-form-field-validation {
    min-height: 20px;
}

11. Monitoring CLS

Track CLS with Analytics

// Add to layout
let clsValue = 0;

new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
            clsValue += entry.value;
        }
    }
}).observe({type: 'layout-shift', buffered: true});

window.addEventListener('beforeunload', () => {
    if (typeof gtag !== 'undefined') {
        gtag('event', 'web_vitals', {
            'metric_name': 'CLS',
            'metric_value': clsValue,
            'metric_rating': clsValue < 0.1 ? 'good' : 'poor',
            'page_template': '@Sitecore.Context.Item.TemplateName'
        });
    }
});

Application Insights CLS Tracking

// Track CLS server-side
var telemetry = new Microsoft.ApplicationInsights.TelemetryClient();
telemetry.TrackMetric("CLS", clsValue);

Testing CLS Improvements

Tools

  1. Chrome DevTools: Performance tab with Layout Shift regions
  2. PageSpeed Insights: https://pagespeed.web.dev/
  3. WebPageTest: https://www.webpagetest.org/
  4. Lighthouse: Built into Chrome DevTools

Sitecore Testing Checklist

  • Test in normal mode (not Experience Editor)
  • Test with cleared HTML cache
  • Test all personalization variants
  • Test on different screen sizes
  • Test with slow 3G throttling
  • Verify published web database

Common Sitecore CLS Issues

Issue Cause Solution
Image shifts No width/height Always specify dimensions
Personalized content Late-loading variants Reserve space, use server-side
Font swap Custom fonts Use font-display: optional
Component loading Lazy-loaded components Reserve space with min-height
Ad injection Dynamic ads Fixed ad containers
Experience Editor Placeholder rendering Test in Preview/Normal mode

CLS Checklist

  • All images have explicit width and height
  • aspect-ratio CSS used where appropriate
  • Personalized components pre-allocate space
  • Web fonts use font-display: swap
  • Carousel/slider has fixed height
  • Ad spaces have reserved dimensions
  • Form validation space reserved
  • Lazy-loaded components have placeholders
  • Tested in normal mode (not Experience Editor)
  • All variants have consistent heights

Quick Fixes

CSS-Only Fix for Most Images

/* Prevent CLS for all images */
img {
    aspect-ratio: attr(width) / attr(height);
    height: auto;
}

Universal Component Wrapper

@* Wrap all components *@
@functions {
    public IHtmlString RenderComponentWithSpace(string placeholderName, int minHeight = 200)
    {
        return new HtmlString($@"
            <div style='min-height: {minHeight}px;'>
                {Html.Sitecore().Placeholder(placeholderName)}
            </div>
        ");
    }
}

@RenderComponentWithSpace("main-content", 300)

Next Steps

// SYS.FOOTER