CLS Prevention: Eliminating Layout Shifts for Better User Experience and SEO

Cumulative Layout Shift (CLS) frustrates users and hurts SEO rankings. Learn systematic approaches to prevent layout shifts and achieve a CLS score under 0.1.

Understanding CLS Calculation

CLS measures the sum of all unexpected layout shift scores. Each shift score = impact fraction × distance fraction.

Expected vs Unexpected Shifts

User-initiated shifts (like clicking a button that expands content) don't count. Only unexpected shifts from:

Image Layout Shift Prevention

Always Set Dimensions

<!-- Bad: No dimensions -->
<img src="photo.jpg" alt="Photo">

<!-- Good: Explicit dimensions -->
<img src="photo.jpg" width="800" height="600" alt="Photo">

<!-- Better: Aspect ratio with CSS -->
<img src="photo.jpg" style="aspect-ratio: 4/3; width: 100%;" alt="Photo">

Responsive Image Containers

.image-container {
  position: relative;
  width: 100%;
  padding-bottom: 56.25%; /* 16:9 aspect ratio */
}
.image-container img {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

Font Loading CLS Prevention

Strategy 1: font-display: optional

@font-face {
  font-family: "MyFont";
  src: url("/fonts/myfont.woff2") format("woff2");
  font-display: optional;
}

This uses the fallback font if the custom font isn't cached. Zero CLS but users may never see the custom font on first visit.

Strategy 2: Size Adjustment

@font-face {
  font-family: "MyFont";
  src: url("/fonts/myfont.woff2") format("woff2");
  font-display: swap;
  size-adjust: 105.2%; /* Match fallback font metrics */
  ascent-override: 98%;
  descent-override: 25%;
  line-gap-override: 0%;
}

Strategy 3: Font Preloading

<link rel="preload" href="/fonts/myfont.woff2" as="font" type="font/woff2" crossorigin>

Dynamic Content CLS Prevention

Reserve Space for Late-Loading Content

<!-- Reserve space for dynamically loaded content -->
<div style="min-height: 300px;" id="recommendations">
  <!-- Content loads here -->
  <div class="placeholder-skeleton">
    <!-- Skeleton loading state -->
  </div>
</div>

Ad Slot Management

<!-- Reserve fixed space for ads -->
<div class="ad-container" style="min-height: 250px;">
  <div id="ad-slot-1"></div>
</div>

Avoid Inserting Content Above Existing Content

// Bad: Pushes content down
container.insertBefore(newElement, container.firstChild);

// Good: Append or replace placeholder
container.appendChild(newElement);
// Or replace a placeholder
placeholder.replaceWith(newElement);

Third-Party Widget CLS Prevention

Facade Pattern

Replace heavy embeds with lightweight previews:

<!-- YouTube facade -->
<div class="video-facade" style="aspect-ratio: 16/9;">
  <button class="play-button" onclick="loadVideo()">
    <img src="thumbnail.jpg" width="480" height="270" alt="Video thumbnail">
    <svg class="play-icon">...</svg>
  </button>
</div>

Monitoring CLS in Production

import {onCLS} from 'web-vitals';

onCLS((metric) => {
  // Log CLS value and shifts
  console.log('CLS:', metric.value);
  metric.entries.forEach((entry) => {
    entry.sources.forEach((source) => {
      console.log('Shift element:', source.node);
    });
  });
});

CLS Optimization Checklist

  1. Set explicit dimensions on all images and videos
  2. Use aspect-ratio CSS property
  3. Implement font-display strategy
  4. Reserve space for dynamic content
  5. Use skeleton loading states
  6. Preallocate ad slots
  7. Implement facade pattern for embeds
  8. Avoid content insertion above fold
  9. Test with Chrome DevTools Layout Shift Regions
  10. Monitor CLS in production with RUM