General Guide: See Global CLS Guide for universal concepts and fixes.
What is CLS?
Cumulative Layout Shift measures visual stability. Google recommends CLS under 0.1. eZ Platform (Ibexa DXP) generates CLS from Landing Page blocks rendering at variable heights, image variations loading with different dimensions, and personalization content swaps.
eZ Platform-Specific CLS Causes
- Landing Page block height variability -- blocks render at different heights depending on content type and field values
- Image variation size mismatches -- if the placeholder or initial render uses different dimensions than the final image variation, the layout shifts
- eZ Personalization content swaps -- the personalization engine replaces default content with targeted variants after initial render
- ESI (Edge Side Include) blocks -- eZ Platform uses ESI for dynamic fragments; these load asynchronously and shift surrounding content
- Rich text field embeds --
<ezembedded>tags in XML fields render at unknown heights
Fixes
1. Reserve Space for Landing Page Blocks
Set minimum heights per block type in your CSS:
/* Landing Page block containment */
.ez-block-hero { min-height: 500px; aspect-ratio: 16/6; contain: layout; }
.ez-block-contentlist { min-height: 400px; contain: layout; }
.ez-block-gallery { min-height: 450px; contain: layout; }
.ez-block-form { min-height: 300px; contain: layout; }
.ez-block-embed { min-height: 200px; contain: layout; }
/* Generic block containment */
[data-ez-block-type] {
contain: layout;
}
Or in your block Twig templates:
{# templates/blocks/content_list.html.twig #}
<div class="ez-block-contentlist" style="min-height: 400px; contain: layout;">
{% for item in items %}
<div class="content-item" style="min-height: 120px;">
{{ render_content(item) }}
</div>
{% endfor %}
</div>
2. Set Image Variation Dimensions
Always output width and height from the variation metadata:
{# Image output with variation dimensions #}
{% set imageField = ez_field_value(content, 'image') %}
{% if imageField is not empty %}
{% set variation = ez_image_alias(imageField, content.versionInfo, 'hero') %}
<img
src="{{ variation.uri }}"
width="{{ variation.width }}"
height="{{ variation.height }}"
alt="{{ imageField.alternativeText }}"
loading="lazy"
style="aspect-ratio: {{ variation.width }} / {{ variation.height }};"
>
{% endif %}
For content listings:
{# Listing with consistent image slots #}
{% for content in pager.currentPageResults %}
<div class="listing-card" style="min-height: 280px;">
<div class="card-image" style="aspect-ratio: 16/9; overflow: hidden; background: #f0f0f0;">
{% set img = ez_field_value(content, 'thumbnail') %}
{% if img is not empty %}
{% set thumb = ez_image_alias(img, content.versionInfo, 'thumbnail') %}
<img src="{{ thumb.uri }}" width="{{ thumb.width }}" height="{{ thumb.height }}"
loading="lazy" style="width:100%;height:100%;object-fit:cover;">
{% endif %}
</div>
<h3>{{ ez_content_name(content) }}</h3>
</div>
{% endfor %}
3. Handle ESI Block CLS
eZ Platform uses ESI for dynamic fragments. Reserve space:
{# Wrap ESI fragments in reserved containers #}
<div class="esi-container" style="min-height: 200px; contain: layout;">
{{ render_esi(controller('App\\Controller\\SidebarController::recommendations')) }}
</div>
/* ESI fragment containers */
.esi-container {
contain: layout;
min-height: 100px;
}
/* User-specific fragments */
.user-panel-esi { min-height: 60px; }
.recommendation-esi { min-height: 300px; }
4. Handle Personalization CLS
eZ Personalization swaps content client-side. Minimize impact:
/* Personalization slot containment */
[data-ez-personalization] {
contain: layout;
min-height: 200px;
}
/* Transition instead of instant swap */
[data-ez-personalization] {
transition: opacity 0.2s ease;
}
[data-ez-personalization].loading {
opacity: 0.5;
}
5. Preload Theme Fonts
{# In pagelayout.html.twig <head> #}
<link rel="preload" href="{{ asset('fonts/brand.woff2') }}" as="font" type="font/woff2" crossorigin>
@font-face {
font-family: 'BrandFont';
src: url('/fonts/brand.woff2') format('woff2');
font-display: swap;
size-adjust: 103%;
}
Measuring CLS on eZ Platform
- Chrome DevTools Performance tab -- look for layout-shift entries, especially during ESI fragment loading
- Test Landing Pages -- these have the most blocks and highest CLS risk
- Test with personalization on/off -- compare CLS with eZ Personalization active vs. disabled
- Symfony Profiler -- shows ESI fragment render order and timing
Analytics Script Impact
- eZ Personalization bundle is the biggest CLS risk from the analytics/personalization side
- GTM/GA should load via
asyncin the pagelayout footer - Cookie consent (eZ Privacy Cookie bundle) should use
position: fixedoverlay