Fix CLS Issues on Kentico (Layout Shift) | OpsBlu Docs

Fix CLS Issues on Kentico (Layout Shift)

Stabilize Kentico layouts by reserving widget zone containers, sizing media library images, and preloading Xperience theme fonts.

Cumulative Layout Shift (CLS) measures visual stability by tracking unexpected layout shifts. This guide provides Kentico-specific solutions to achieve a CLS score under 0.1.

What is CLS?

CLS measures: The sum of all unexpected layout shift scores during page load.

Good CLS scores:

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

Common causes:

  • Images without dimensions
  • Ads, embeds, iframes without reserved space
  • Dynamically injected content
  • Web fonts causing FOIT/FOUT
  • Actions waiting for network response

Diagnosing CLS Issues

Using PageSpeed Insights

  1. Visit https://pagespeed.web.dev/
  2. Enter your Kentico site URL
  3. Review Diagnostics section
  4. Look for "Avoid large layout shifts"
  5. Identify which elements cause shifts

Using Chrome DevTools

  1. Open DevTools (F12)
  2. Performance tab → Capture
  3. Enable Web Vitals in settings
  4. Look for red Layout Shift bars
  5. Click to see which elements shifted

Layout Shift Regions

Enable layout shift visualization:

// Add to console or page
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      console.log('Layout shift:', entry);
      console.log('Elements affected:', entry.sources);
    }
  }
}).observe({type: 'layout-shift', buffered: true});

Kentico-Specific CLS Fixes

1. Fix Images from Media Library

The most common CLS issue - images loading without dimensions.

Always Set Width and Height

@using CMS.MediaLibrary
@{
    var mediaFile = MediaFileInfoProvider.GetMediaFileInfo(imageGuid);
}

<img src="@MediaFileURLProvider.GetMediaFileUrl(mediaFile.FileGUID, mediaFile.FileName)"
     width="@mediaFile.FileImageWidth"
     height="@mediaFile.FileImageHeight"
     alt="@mediaFile.FileDescription"
     loading="lazy">

Create Helper for Responsive Images

@using CMS.MediaLibrary
@using CMS.Helpers

@helper RenderResponsiveImage(Guid imageGuid, string altText, string cssClass = "")
{
    var mediaFile = MediaFileInfoProvider.GetMediaFileInfo(imageGuid);
    if (mediaFile != null)
    {
        var aspectRatio = (double)mediaFile.FileImageHeight / mediaFile.FileImageWidth * 100;

        <div class="responsive-image-wrapper @cssClass" style="padding-bottom: @(aspectRatio)%;">
            <img src="@MediaFileURLProvider.GetMediaFileUrl(mediaFile.FileGUID, mediaFile.FileName)"
                 alt="@altText"
                 loading="lazy"
                 style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover;">
        </div>
    }
}

@* Usage: *@
@RenderResponsiveImage(Model.HeroImageGuid, "Hero image", "hero-image")

Use Aspect Ratio CSS

@{
    var mediaFile = MediaFileInfoProvider.GetMediaFileInfo(imageGuid);
    var aspectRatio = $"{mediaFile.FileImageWidth} / {mediaFile.FileImageHeight}";
}

<img src="@MediaFileURLProvider.GetMediaFileUrl(mediaFile.FileGUID, mediaFile.FileName)"
     style="aspect-ratio: @aspectRatio; width: 100%; height: auto;"
     alt="@mediaFile.FileDescription">

2. Fix Background Images

Background images can cause layout shifts if container height isn't set.

Set Container Dimensions

@{
    var heroImage = DocumentContext.CurrentDocument.GetValue("HeroImage");
    var heroImageFile = MediaFileInfoProvider.GetMediaFileInfo((Guid)heroImage);
    var aspectRatio = (double)heroImageFile.FileImageHeight / heroImageFile.FileImageWidth * 100;
}

<section class="hero" style="padding-bottom: @(aspectRatio)%; position: relative;">
    <div style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;
                background-image: url('@MediaFileURLProvider.GetMediaFileUrl(heroImageFile.FileGUID, heroImageFile.FileName)');
                background-size: cover; background-position: center;">
        <!-- Content -->
    </div>
</section>

Or Use Fixed Heights

.hero-section {
  height: 500px; /* Or use vh units */
  background-image: url('...');
  background-size: cover;
  background-position: center;
}

@media (max-width: 768px) {
  .hero-section {
    height: 300px;
  }
}

3. Fix Web Parts (Portal Engine)

Portal Engine web parts can cause shifts when loading dynamically.

Reserve Space for Web Parts

<!-- In web part zone -->
<cms:CMSWebPartZone ID="zoneMain" runat="server" style="min-height: 400px;">
    <cms:CMSWebPart ID="productListing" runat="server" />
</cms:CMSWebPartZone>

Use Skeleton Screens

<div class="webpart-container">
    <div class="skeleton" id="skeleton_<%= ClientID %>">
        <!-- Skeleton placeholder -->
        <div class="skeleton-title"></div>
        <div class="skeleton-text"></div>
        <div class="skeleton-text"></div>
    </div>

    <div class="webpart-content" style="display: none;" id="content_<%= ClientID %>">
        <!-- Actual content -->
    </div>
</div>

<script>
  document.addEventListener('DOMContentLoaded', function() {
    // Hide skeleton, show content when ready
    document.getElementById('skeleton_<%= ClientID %>').style.display = 'none';
    document.getElementById('content_<%= ClientID %>').style.display = 'block';
  });
</script>

4. Fix Font Loading (FOUT/FOIT)

Web fonts can cause text to shift when they load.

Use font-display

@font-face {
  font-family: 'CustomFont';
  src: url('/fonts/CustomFont.woff2') format('woff2');
  font-display: swap; /* or 'optional' */
}

Preload Critical Fonts

<head>
    <link rel="preload"
          href="~/Content/fonts/primary-font.woff2"
          as="font"
          type="font/woff2"
          crossorigin>

    <style>
      @font-face {
        font-family: 'PrimaryFont';
        src: url('/Content/fonts/primary-font.woff2') format('woff2');
        font-display: swap;
      }
    </style>
</head>

Match Fallback Font Metrics

body {
  font-family: 'CustomFont', Arial, sans-serif;
  /* Adjust fallback font to match custom font metrics */
  font-size: 16px;
  line-height: 1.5;
}

/* Use size-adjust to match fallback */
@font-face {
  font-family: 'CustomFont-Fallback';
  src: local('Arial');
  size-adjust: 105%; /* Adjust to match custom font */
}

body {
  font-family: 'CustomFont', 'CustomFont-Fallback', Arial, sans-serif;
}

5. Fix Dynamic Content Injection

Kentico widgets and dynamic content can cause shifts.

Reserve Space for Dynamic Content

<div class="dynamic-content-area" style="min-height: 300px;">
    @* Dynamic content will load here *@
</div>

Use MVC Widget with Fixed Dimensions

@using Kentico.PageBuilder.Web.Mvc

<div class="widget-container" style="min-height: @Model.MinHeight">
    @if (Model.IsLoaded)
    {
        <!-- Widget content -->
    }
    else
    {
        <!-- Placeholder with same dimensions -->
        <div class="widget-placeholder" style="height: @Model.MinHeight">
            Loading...
        </div>
    }
</div>

6. Fix Ads and Third-Party Embeds

Ads and embeds are common CLS culprits.

Reserve Space for Ads

<div class="ad-container" style="width: 728px; height: 90px; background: #f0f0f0;">
    <div id="ad-slot-1">
        <!-- Ad code -->
    </div>
</div>

For Responsive Ads

<div class="ad-wrapper" style="padding-bottom: 12.36%; position: relative;">
    <!-- 12.36% = 90/728 for 728x90 ad -->
    <div style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;">
        <!-- Ad code -->
    </div>
</div>

YouTube Embeds

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

7. Fix Kentico Page Builder

Page Builder widgets can cause layout shifts.

Set Widget Section Heights

@* In your section/widget view *@
<div class="widget-section" data-widget-section style="min-height: 400px;">
    @RenderBody()
</div>

Create Stable Widget Containers

// In your widget model
public class WidgetViewModel
{
    public int MinHeight { get; set; } = 300;
    public string AspectRatio { get; set; } = "16/9";
}
@model WidgetViewModel

<div class="widget-wrapper" style="aspect-ratio: @Model.AspectRatio; min-height: @(Model.MinHeight)px;">
    <!-- Widget content -->
</div>

8. Fix Navigation Menus

Menus that load or transform can cause shifts.

Reserve Space for Dropdowns

.nav-dropdown {
  position: absolute; /* Don't push content down */
  top: 100%;
  left: 0;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.2s, visibility 0.2s;
}

.nav-item:hover .nav-dropdown {
  opacity: 1;
  visibility: visible;
}

Prevent Mobile Menu Shift

/* Reserve space for mobile menu toggle */
.mobile-menu-toggle {
  width: 44px; /* Fixed width */
  height: 44px; /* Fixed height */
}

/* Overlay menu instead of pushing content */
.mobile-menu {
  position: fixed;
  top: 0;
  left: -100%;
  width: 80%;
  height: 100vh;
  transition: left 0.3s;
}

.mobile-menu.open {
  left: 0;
}

9. Fix Form Validation Messages

Error messages that appear can cause shifts.

Reserve Space for Error Messages

<div class="form-group">
    @Html.LabelFor(m => m.Email)
    @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })

    <div class="error-message" style="min-height: 20px;">
        @Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" })
    </div>
</div>

Or Use Absolute Positioning

.form-group {
  position: relative;
  margin-bottom: 30px; /* Space for error message */
}

.error-message {
  position: absolute;
  bottom: -25px;
  left: 0;
  color: red;
  font-size: 14px;
}

10. Fix Kentico Macros

Macros that resolve slowly can cause shifts.

Cache Macro Results

@{
    // Cache expensive macro results
    var macroResult = CacheHelper.Cache(cs =>
    {
        var result = MacroResolver.Resolve("{% CurrentDocument.DocumentName %}");
        if (cs.Cached)
        {
            cs.CacheDependency = CacheHelper.GetCacheDependency("node|" + DocumentContext.CurrentDocument.NodeID);
        }
        return result;
    }, new CacheSettings(60, "macroResult", DocumentContext.CurrentDocument.NodeID));
}

<div style="min-height: 50px;">
    @Html.Raw(macroResult)
</div>

CSS Techniques for Preventing CLS

1. Aspect Ratio Boxes

.aspect-ratio-box {
  position: relative;
  width: 100%;
  padding-bottom: 56.25%; /* 16:9 ratio */
}

.aspect-ratio-box > * {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

2. Min-Height for Dynamic Content

.dynamic-content {
  min-height: 300px;
}

@media (max-width: 768px) {
  .dynamic-content {
    min-height: 200px;
  }
}

3. Transform Instead of Position

/* BAD: Changes position, causes shift */
.dropdown {
  display: none;
}
.dropdown.active {
  display: block;
}

/* GOOD: Uses transform, no shift */
.dropdown {
  transform: translateY(-100%);
  opacity: 0;
  visibility: hidden;
  transition: transform 0.2s, opacity 0.2s;
}
.dropdown.active {
  transform: translateY(0);
  opacity: 1;
  visibility: visible;
}

JavaScript Best Practices

1. Set Dimensions Before Loading

// Get image from Kentico Media Library
fetch('/api/media/info?guid=' + imageGuid)
  .then(response => response.json())
  .then(data => {
    var img = document.createElement('img');
    img.width = data.width;  // Set dimensions first
    img.height = data.height;
    img.src = data.url;      // Then load image
    container.appendChild(img);
  });

2. Batch DOM Changes

// BAD: Multiple reflows
container.style.height = '200px';
container.style.width = '300px';
container.classList.add('active');

// GOOD: Single reflow
container.style.cssText = 'height: 200px; width: 300px;';
container.classList.add('active');

3. Use RequestAnimationFrame

function updateLayout() {
  requestAnimationFrame(() => {
    // DOM changes here
    element.style.height = newHeight + 'px';
  });
}

Testing CLS Fixes

1. Web Vitals JavaScript Library

<script type="module">
  import {getCLS} from 'https://unpkg.com/web-vitals@3/dist/web-vitals.js';

  getCLS(({name, value, rating}) => {
    console.log('CLS:', value, rating);

    // Send to analytics
    gtag('event', 'web_vitals', {
      'metric_name': name,
      'metric_value': value,
      'metric_rating': rating
    });
  });
</script>

2. Layout Shift Recorder

<script>
  let clsScore = 0;
  const clsObserver = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      if (!entry.hadRecentInput) {
        clsScore += entry.value;
        console.log('Layout Shift:', entry.value);
        console.log('Total CLS:', clsScore);
        console.log('Shifted elements:', entry.sources);
      }
    }
  });

  clsObserver.observe({type: 'layout-shift', buffered: true});

  // Report final CLS
  window.addEventListener('beforeunload', () => {
    console.log('Final CLS Score:', clsScore);
  });
</script>

3. PageSpeed Insights API

// Automate testing
const url = 'https://www.googleapis.com/pagespeedonline/v5/runPagespeed';
const params = new URLSearchParams({
  url: 'https://yoursite.com',
  key: 'YOUR_API_KEY'
});

fetch(`${url}?${params}`)
  .then(response => response.json())
  .then(data => {
    const cls = data.lighthouseResult.audits['cumulative-layout-shift'].numericValue;
    console.log('CLS Score:', cls);
  });

Common CLS Issues and Solutions

Issue: Images Loading Without Dimensions

Solution:

  • Always set width/height attributes
  • Use aspect-ratio CSS
  • Include dimensions in Media Library queries

Issue: Web Fonts Causing Shift

Solution:

  • Use font-display: swap or optional
  • Preload critical fonts
  • Match fallback font metrics

Issue: Ads Causing Shift

Solution:

  • Reserve fixed space for ad slots
  • Use padding-bottom technique
  • Consider lazy loading ads

Issue: Dynamic Content Insertion

Solution:

  • Reserve min-height for containers
  • Use skeleton screens
  • Batch DOM updates

Issue: Portal Engine ViewState

Solution:

<appSettings>
  <add key="CMSControlState" value="false" />
</appSettings>

Monitoring CLS

Google Search Console

  1. Go to Core Web Vitals report
  2. Check CLS metrics
  3. Identify problematic URLs
  4. Fix and request re-crawl

Real User Monitoring

<script type="module">
  import {getCLS} from 'https://unpkg.com/web-vitals@3/dist/web-vitals.js';

  getCLS(({value}) => {
    // Send to your analytics
    fetch('/api/metrics', {
      method: 'POST',
      body: JSON.stringify({
        metric: 'CLS',
        value: value,
        url: window.location.href
      })
    });
  });
</script>

Best Practices

  1. Always Set Image Dimensions: Width and height attributes
  2. Reserve Space: For dynamic content, ads, embeds
  3. Optimize Fonts: Use font-display and preload
  4. Avoid Injection: Don't insert content above existing content
  5. Use Transforms: Instead of changing position/dimensions
  6. Test on Real Devices: Mobile devices especially prone to CLS
  7. Monitor Continuously: Track CLS metrics over time
  8. Fix Highest Impact: Focus on most-visited pages first

Additional Resources