Cumulative Layout Shift (CLS) measures visual stability. Joomla sites often suffer from CLS due to dynamic module insertion, web fonts, template layouts, and extension-injected content. This guide provides Joomla-specific solutions.
What is CLS?
CLS is a Core Web Vital that measures unexpected layout shifts during page load.
Thresholds:
- Good: < 0.1
- Needs Improvement: 0.1 - 0.25
- Poor: > 0.25
Common CLS Causes on Joomla:
- Images without width/height attributes
- Web fonts loading (FOUT/FOIT)
- Dynamic module/ad insertion
- Cookie consent banners
- Lazy-loaded images
- Embeds (YouTube, Twitter, Instagram)
Measuring Joomla CLS
Google PageSpeed Insights
- Go to PageSpeed Insights
- Enter your Joomla URL
- View Cumulative Layout Shift metric
- Check Diagnostics → Avoid large layout shifts for specific elements
Chrome DevTools
Layout Shift Regions (Visual):
- Open DevTools → Performance tab
- Check Experience checkbox
- Click Record and reload page
- Look for red "Layout Shift" markers
- Click marker to see which element shifted
Console Logging:
// Log all layout shifts
let cls = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
cls += entry.value;
console.log('Layout Shift:', entry.value, 'Total CLS:', cls);
console.log('Shifted elements:', entry.sources);
}
}
}).observe({type: 'layout-shift', buffered: true});
Web Vitals Chrome Extension
Install Web Vitals to see CLS on every Joomla page.
Common Joomla CLS Issues
1. Images Without Dimensions
Symptoms:
- Content jumps down as images load
- Article images cause shifts
- VirtueMart/J2Store product images shift layout
Solution: Always Set Width & Height
In Joomla articles:
<!-- Always include width and height -->
<img src="images/article-image.jpg" width="800" height="600" alt="Article Image">
In template:
<?php
// Ensure images have dimensions
$image = JImage::getInstance($imagePath);
$properties = $image->getImageFileProperties($imagePath);
?>
<img src="<?php echo $imagePath; ?>"
width="<?php echo $properties->width; ?>"
height="<?php echo $properties->height; ?>"
alt="Image">
CSS aspect-ratio:
/* In template CSS */
img {
max-width: 100%;
height: auto;
}
.article-image {
aspect-ratio: 16 / 9;
width: 100%;
}
2. Lazy Loading Images
Symptoms:
- Images above the fold cause layout shifts
- Lazy load placeholder → full image causes jump
Solutions:
Don't Lazy Load Above-the-Fold Images
<!-- Hero/featured images: eager loading -->
<img src="hero.jpg" width="1920" height="1080" loading="eager" fetchpriority="high" alt="Hero">
<!-- Below the fold: lazy loading -->
<img src="content.jpg" width="800" height="400" loading="lazy" alt="Content">
Disable lazy loading for specific images in JCH Optimize:
Components → JCH Optimize → Settings
Image Optimization:
- Lazy Load Images: Yes
- Exclude Images: Add hero image classes/IDs
Use Placeholder with Same Dimensions
<!-- SVG placeholder with aspect ratio -->
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 9'%3E%3C/svg%3E"
data-src="actual-image.jpg"
width="1600"
height="900"
class="lazy"
alt="Image">
3. Web Fonts Loading (FOUT/FOIT)
Symptoms:
- Text appears, then changes font (Flash of Unstyled Text)
- Text invisible, then appears (Flash of Invisible Text)
- Headers/body text jumps when font loads
Solutions:
Use font-display: swap
@font-face {
font-family: 'Your Custom Font';
src: url('/templates/your-template/fonts/font.woff2') format('woff2');
font-display: swap; /* Show fallback immediately */
}
For Google Fonts:
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">
Preload Critical Fonts
// In template index.php
<?php
$doc = JFactory::getDocument();
$templatePath = $this->baseurl . '/templates/' . $this->template;
$doc->addHeadLink($templatePath . '/fonts/primary-font.woff2', 'preload', 'rel', [
'as' => 'font',
'type' => 'font/woff2',
'crossorigin' => 'anonymous'
]);
?>
Use System Fonts (No CLS)
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
}
4. Dynamic Module/Ad Insertion
Symptoms:
- Content jumps as modules load
- Sidebar modules push content down
- In-content ads shift paragraphs
Solutions:
Reserve Module Space with Min-Height
/* In template CSS */
.moduletable {
min-height: 250px; /* Match typical module height */
}
/* For specific module positions */
#sidebar .moduletable {
min-height: 300px;
}
For ads:
.ad-module {
min-height: 250px;
min-width: 300px;
background: #f5f5f5; /* Placeholder */
}
/* Responsive ads */
.ad-module {
aspect-ratio: 300 / 250;
width: 100%;
max-width: 300px;
}
Load Modules Inline (Not Via AJAX)
If modules load via AJAX, they cause CLS. Load them inline instead:
<!-- In template position -->
<jdoc:include type="modules" name="sidebar" style="xhtml" />
5. Cookie Consent Banners
Symptoms:
- Banner pushes content down on first visit
- Banner appears after page loads
Solutions:
Position Banner as Overlay
/* Don't push content - overlay instead */
.cookie-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 9999;
/* This doesn't push content, no CLS */
}
Popular Joomla Cookie Extensions:
- CookieNotice - Configure to overlay
- iubenda - Set position: fixed
- Complianz GDPR/CCPA - Use overlay mode
6. Template Layout Changes
Symptoms:
- Template elements jump/resize on load
- Menu changes size
- Headers/footers shift
Solutions:
Define Heights for Template Elements
/* Template CSS */
.site-header {
min-height: 80px; /* Prevents collapse */
}
.main-navigation {
height: 60px;
}
.site-footer {
min-height: 200px;
}
Use CSS Grid/Flexbox with Fixed Dimensions
.site-wrapper {
display: grid;
grid-template-rows: 80px 1fr 200px; /* header, content, footer */
grid-template-columns: 250px 1fr; /* sidebar, main */
min-height: 100vh;
}
7. Joomla Module Chrome
Symptoms:
- Modules change style/layout after load
- Module titles/styling shift
Solutions:
Optimize Module Chrome
// In templates/your-template/html/modules.php
<?php
defined('_JEXEC') or die;
function modChrome_optimized($module, &$params, &$attribs)
{
// Consistent module structure with fixed dimensions
$moduleClass = htmlspecialchars($params->get('moduleclass_sfx'));
?>
<div class="moduletable <?php echo $moduleClass; ?>" style="min-height: 100px;">
<?php if ($module->showtitle) : ?>
<h3 class="module-title"><?php echo $module->title; ?></h3>
<?php endif; ?>
<div class="module-content">
<?php echo $module->content; ?>
</div>
</div>
<?php
}
8. Embeds (YouTube, Social Media)
Symptoms:
- Embedded videos cause layout shift
- Social media embeds (Twitter, Facebook) jump
Solutions:
Reserve Space with Aspect Ratio
<!-- YouTube embed with 16:9 aspect ratio -->
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
<iframe src="https://www.youtube.com/embed/VIDEO_ID"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"
frameborder="0"
allowfullscreen>
</iframe>
</div>
CSS solution:
.video-embed {
position: relative;
aspect-ratio: 16 / 9;
width: 100%;
}
.video-embed iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
Use Lazy Load for Embeds
<!-- Load iframe on click -->
<div class="video-placeholder" data-video-id="VIDEO_ID" style="aspect-ratio: 16/9; background: url('thumbnail.jpg');">
<button Video</button>
</div>
<script>
function loadVideo(button) {
const container = button.parentElement;
const videoId = container.getAttribute('data-video-id');
container.innerHTML = `<iframe src="https://www.youtube.com/embed/${videoId}?autoplay=1" style="width:100%;height:100%;" allowfullscreen></iframe>`;
}
</script>
9. VirtueMart/J2Store/HikaShop Product Pages
Symptoms:
- Product images shift layout
- Price/availability info jumps
- Add to cart button moves
Solutions:
Reserve Product Image Space
/* VirtueMart */
.product-container .vm-product-media-container {
aspect-ratio: 1 / 1; /* Square product images */
width: 100%;
}
/* J2Store */
.j2store-product-images {
min-height: 400px;
aspect-ratio: 1 / 1;
}
/* HikaShop */
.hikashop_product_image {
aspect-ratio: 1 / 1;
width: 100%;
}
Set Fixed Heights for Product Info
.product-price {
min-height: 30px;
}
.product-availability {
min-height: 20px;
}
.addtocart-bar {
min-height: 50px;
}
Advanced Joomla CLS Fixes
CSS Containment
/* Limit layout impact of dynamic content */
.module-container {
contain: layout style;
}
.article-content {
contain: layout;
}
Transform Instead of Layout Properties
/* Use transform for animations (no CLS) */
.dropdown-menu {
transform: translateY(-100%);
transition: transform 0.3s;
}
.dropdown-menu.open {
transform: translateY(0);
}
/* Don't use top/height (causes CLS) */
.dropdown-menu-bad {
top: -100px; /* Causes layout shift */
transition: top 0.3s;
}
Preload Key Resources
// In template index.php
<?php
$doc = JFactory::getDocument();
// Preload critical CSS
$doc->addHeadLink($this->baseurl . '/templates/' . $this->template . '/css/template.css', 'preload', 'rel', ['as' => 'style']);
// Preload fonts
$doc->addHeadLink($this->baseurl . '/templates/' . $this->template . '/fonts/main.woff2', 'preload', 'rel', [
'as' => 'font',
'type' => 'font/woff2',
'crossorigin' => 'anonymous'
]);
?>
Testing CLS Improvements
Before/After Comparison
Baseline Measurement:
- Run PageSpeed Insights
- Record CLS score
- Note specific shifting elements
Make Changes
Re-test:
- Clear all caches
- Run PageSpeed Insights
- Compare CLS improvement
Layout Shift GIF
Use Web Vitals extension to create visual layout shift GIF:
1. Install extension
2. Open your Joomla site
3. Click extension icon
4. View Layout Shifts
5. Generate GIF of shifts
Real User Monitoring
// Track CLS in production
let clsValue = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
}
// Send to analytics
if (typeof gtag !== 'undefined') {
gtag('event', 'cls_measured', {
'value': clsValue,
'page': window.location.pathname
});
}
}).observe({type: 'layout-shift', buffered: true});
Joomla CLS Checklist
- All images have width and height attributes
- Don't lazy load above-the-fold images
- Use font-display: swap for web fonts
- Preload critical fonts
- Reserve space for ads/modules with min-height
- Position cookie banners as fixed overlay
- Set aspect ratio for embeds (YouTube, etc.)
- Define template element heights (header, footer)
- Optimize module chrome
- Use CSS containment for dynamic content
- Test with JCH Optimize image optimization
- Check product pages (VirtueMart/J2Store/HikaShop)
Next Steps
- Fix LCP Issues - Largest Contentful Paint
- Debug Tracking Issues
- Review Global CLS Guide - Universal CLS concepts
Related Resources
- Joomla Template Development
- Core Web Vitals - Universal performance concepts
- Web.dev CLS Guide - Detailed CLS documentation