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

Fix CLS Issues on Ezplatform (Layout Shift)

Stabilize eZ Platform layouts by reserving Landing Page block containers, sizing image variations, and preloading Ibexa theme fonts.

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

  1. Chrome DevTools Performance tab -- look for layout-shift entries, especially during ESI fragment loading
  2. Test Landing Pages -- these have the most blocks and highest CLS risk
  3. Test with personalization on/off -- compare CLS with eZ Personalization active vs. disabled
  4. 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 async in the pagelayout footer
  • Cookie consent (eZ Privacy Cookie bundle) should use position: fixed overlay