Fix Adobe Experience Manager CLS Issues | OpsBlu Docs

Fix Adobe Experience Manager CLS Issues

Stabilize AEM layouts by reserving space for Experience Fragments, preloading ClientLib fonts, and sizing DAM asset images.

General Guide: See Global CLS Guide for universal concepts and fixes.

AEM-Specific CLS Causes

1. Experience Fragments Loading

Async Experience Fragment injection:

<!-- Problem: Fragment loads after page render -->
<div data-sly-resource="${'xf' @ resourceType='cq/experience-fragments/editor/components/experiencefragment'}">
</div>

2. Personalization Content

Target/Adobe Target content swapping:

<!-- Content changes after personalization loads -->
<div class="personalization-container">
    ${personalizedContent}
</div>

3. DAM Images Without Dimensions

Images without explicit size:

<!-- Problem: No dimensions specified -->
<img src="${asset.path}"/>

4. Web Fonts (Adobe Fonts)

Font loading causing text reflow:

/* FOUT from Adobe Fonts */
@import url("https://use.typekit.net/abc123.css");

AEM-Specific Fixes

Fix 1: Reserve Space for Experience Fragments

Set minimum dimensions:

<div class="xf-container" style="min-height: 300px;">
    <sly data-sly-resource="${'xf' @ resourceType='cq/experience-fragments/editor/components/experiencefragment'}"/>
</div>

Or use skeleton screens:

<div class="xf-container">
    <sly data-sly-resource="${'xf' @ resourceType='...'}"
         data-sly-unwrap="${xf.content}">
        <!-- Skeleton fallback -->
        <div class="skeleton-xf" data-sly-test="${!xf.content}">
            <div class="skeleton-block"></div>
            <div class="skeleton-block"></div>
        </div>
    </sly>
</div>
.xf-container {
    min-height: 300px;
}

.skeleton-xf {
    animation: pulse 1.5s infinite;
}

.skeleton-block {
    height: 100px;
    background: #f0f0f0;
    margin-bottom: 16px;
    border-radius: 4px;
}

Fix 2: Pre-hide Personalization Containers

Use CSS to prevent layout shift:

/* Pre-hide until personalization loads */
.personalization-container {
    opacity: 0;
    transition: opacity 0.2s ease;
    min-height: 200px;
}

.personalization-container.loaded {
    opacity: 1;
}
// Client-side
window.targetPageParams = function() {
    return {
        "at_property": "your-property-token"
    };
};

// After Target delivers
adobe.target.getOffers({}).then(function() {
    document.querySelectorAll('.personalization-container').forEach(function(el) {
        el.classList.add('loaded');
    });
});

Fix 3: Use Core Image with Dimensions

Core Image component handles dimensions:

<div data-sly-resource="${'image' @ resourceType='core/wcm/components/image/v3/image'}">
</div>

Or specify in custom components:

<sly data-sly-use.asset="${'com.mysite.core.models.AssetModel'}">
    <img src="${asset.src}"
         width="${asset.width}"
         height="${asset.height}"
         alt="${asset.alt}"
         loading="lazy"
         style="aspect-ratio: ${asset.width}/${asset.height};">
</sly>
@Model(adaptables = Resource.class)
public class AssetModel {

    @Inject
    private Resource resource;

    public int getWidth() {
        Asset asset = resource.adaptTo(Asset.class);
        return (int) asset.getMetadataValue("tiff:ImageWidth");
    }

    public int getHeight() {
        Asset asset = resource.adaptTo(Asset.class);
        return (int) asset.getMetadataValue("tiff:ImageLength");
    }
}

Fix 4: Optimize Font Loading

Use font-display for Adobe Fonts:

<!-- Preload critical font -->
<link rel="preload"
      href="https://use.typekit.net/af/font-file.woff2"
      as="font"
      type="font/woff2"
      crossorigin>

<style>
    @font-face {
        font-family: 'MySiteFont';
        src: url('https://use.typekit.net/af/font-file.woff2') format('woff2');
        font-display: optional;
    }
</style>

Or use system font fallback:

body {
    font-family: 'Adobe Clean', -apple-system, BlinkMacSystemFont, sans-serif;
}

Fix 5: Stabilize Header/Navigation

Fixed dimensions for navigation:

.site-header {
    height: 80px; /* Fixed height */
    position: fixed;
    top: 0;
    width: 100%;
}

.main-content {
    padding-top: 80px; /* Match header height */
}

/* User account area */
.user-account {
    min-width: 120px; /* Prevent shift when logged in */
}

Fix 6: Handle Lazy-Loaded Components

Use IntersectionObserver with placeholders:

// Core Components lazy loading
var lazyComponents = document.querySelectorAll('[data-cmp-lazy]');

var observer = new IntersectionObserver(function(entries) {
    entries.forEach(function(entry) {
        if (entry.isIntersecting) {
            var component = entry.target;
            // Maintain dimensions during load
            component.style.minHeight = component.offsetHeight + 'px';

            loadComponent(component).then(function() {
                component.style.minHeight = '';
            });

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

lazyComponents.forEach(function(el) {
    observer.observe(el);
});

Monitoring CLS

import {onCLS} from 'web-vitals';

onCLS(function(metric) {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({
        'event': 'web_vitals',
        'vital_name': 'CLS',
        'vital_value': metric.value,
        'vital_rating': metric.rating
    });
});

CLS Targets

Rating CLS Value
Good ≤ 0.1
Needs Improvement 0.1 - 0.25
Poor > 0.25

Next Steps