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
- Open Chrome DevTools (F12)
- Go to Performance tab
- Check Experience section
- Look for Layout Shift events (red bars)
- Click on shifts to see affected elements
Layout Shift Regions
Enable layout shift regions in DevTools:
- Press
Cmd/Ctrl + Shift + P - Type "Rendering"
- Select "Show Rendering"
- 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>
8. Carousel and Slider Fixes
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
- Chrome DevTools: Performance tab with Layout Shift regions
- PageSpeed Insights: https://pagespeed.web.dev/
- WebPageTest: https://www.webpagetest.org/
- 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)