Cumulative Layout Shift (CLS) to jedna z trzech metryk Core Web Vitals, która mierzy stabilność wizualną strony. Niespodziewane przesunięcia elementów frustrują użytkowników i mogą negatywnie wpłynąć na pozycje w Google. Ten przewodnik pomoże Ci zidentyfikować i naprawić problemy z CLS.

Czym jest CLS?

CLS mierzy sumę wszystkich nieoczekiwanych przesunięć layoutu, które występują podczas całego cyklu życia strony. Przesunięcie jest “nieoczekiwane”, gdy element zmienia pozycję bez interakcji użytkownika (np. kliknięcia).

Jak obliczany jest CLS?

CLS = suma (impact fraction × distance fraction) dla każdego przesunięcia

  • Impact fraction - procent viewportu zajmowany przez przesunięty element
  • Distance fraction - odległość przesunięcia jako procent viewportu

Progi CLS

WynikOcena
≤ 0.1Dobry (zielony)
0.1 - 0.25Wymaga poprawy (pomarańczowy)
> 0.25Słaby (czerwony)

Najczęstsze przyczyny CLS

1. Obrazy bez wymiarów

Gdy przeglądarka nie zna wymiarów obrazu, rezerwuje 0px wysokości, a po załadowaniu obrazu następuje przesunięcie.

<!-- Źle - brak wymiarów -->
<img src="photo.jpg" alt="Zdjęcie">

<!-- Dobrze - wymiary określone -->
<img src="photo.jpg" alt="Zdjęcie" width="800" height="600">

<!-- Dobrze - aspect-ratio w CSS -->
<img src="photo.jpg" alt="Zdjęcie" style="aspect-ratio: 4/3; width: 100%;">

2. Reklamy i embedy

Reklamy często ładują się z opóźnieniem i mają dynamiczną wysokość.

<!-- Rezerwuj miejsce dla reklamy -->
<div class="ad-container" style="min-height: 250px;">
  <!-- Kod reklamowy -->
</div>

3. Dynamicznie wstrzykiwana treść

Banery cookie, powiadomienia push, toolbary - wszystko co pojawia się po załadowaniu strony.

/* Zamiast przesuwać treść, użyj overlay */
.cookie-banner {
  position: fixed;
  bottom: 0;
  /* NIE używaj position: relative na górze strony */
}

4. Web fonty (FOUT/FOIT)

Zamiana fontu fallback na docelowy może zmienić rozmiar tekstu.

/* Dopasuj fallback do docelowego fontu */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: swap;
  /* Użyj size-adjust dla dopasowania */
  size-adjust: 100%;
  ascent-override: 90%;
  descent-override: 20%;
  line-gap-override: 0%;
}

/* Lub użyj font-display: optional */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: optional; /* Nie pokazuj fallback jeśli font się nie załaduje szybko */
}

5. Animacje powodujące reflow

Animacje width, height, top, left powodują reflow i mogą wpływać na CLS.

/* Źle - animacja height */
.menu {
  transition: height 0.3s;
}

/* Dobrze - animacja transform */
.menu {
  transition: transform 0.3s;
  transform-origin: top;
}
.menu.collapsed {
  transform: scaleY(0);
}

Narzędzia do debugowania CLS

1. Chrome DevTools - Performance

  1. Otwórz DevTools (F12)
  2. Przejdź do zakładki Performance
  3. Zaznacz Web Vitals
  4. Naciśnij Ctrl+Shift+E (nagrywanie z przeładowaniem)
  5. Szukaj czerwonych znaczników Layout Shift

2. Chrome DevTools - Rendering

  1. Otwórz DevTools
  2. Naciśnij Ctrl+Shift+P
  3. Wpisz “Show Rendering”
  4. Zaznacz Layout Shift Regions
  5. Niebieskie prostokąty pokazują przesunięcia w czasie rzeczywistym

3. Web Vitals Extension

Rozszerzenie do Chrome pokazujące CLS, LCP i INP w czasie rzeczywistym: Web Vitals Extension

4. Lighthouse

# CLI
npx lighthouse https://example.com --only-categories=performance

# Lub w Chrome DevTools → Lighthouse

5. PageSpeed Insights

https://pagespeed.web.dev/ - pokazuje CLS z danych laboratoryjnych i rzeczywistych (CrUX).

6. Layout Instability API

Programowe wykrywanie przesunięć:

// Nasłuchuj na layout shifts
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      console.log('Layout shift detected:', entry);
      console.log('Value:', entry.value);
      console.log('Sources:', entry.sources);

      // Pokaż które elementy się przesunęły
      entry.sources?.forEach(source => {
        console.log('Element:', source.node);
        console.log('Previous rect:', source.previousRect);
        console.log('Current rect:', source.currentRect);
      });
    }
  }
});

observer.observe({ type: 'layout-shift', buffered: true });

7. web-vitals library

import { onCLS } from 'web-vitals';

onCLS(console.log, { reportAllChanges: true });
// { name: 'CLS', value: 0.15, rating: 'needs-improvement', entries: [...] }

Techniki naprawy CLS

1. Rezerwacja miejsca dla obrazów

Metoda 1: width i height atrybuty

<img src="hero.jpg" width="1200" height="600" alt="Hero">

Przeglądarka obliczy aspect ratio i zarezerwuje miejsce.

Metoda 2: aspect-ratio CSS

.hero-image {
  aspect-ratio: 16 / 9;
  width: 100%;
  height: auto;
}

Metoda 3: Padding hack (legacy)

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

2. Rezerwacja miejsca dla reklam

.ad-slot {
  min-height: 250px; /* Standardowa wysokość reklamy */
  background: #f0f0f0; /* Placeholder */
}

/* Dla responsywnych reklam */
.ad-slot-responsive {
  min-height: 100px;
}
@media (min-width: 768px) {
  .ad-slot-responsive {
    min-height: 250px;
  }
}

3. Skeleton screens

Zamiast pustego miejsca, pokaż skeleton:

<div class="card skeleton">
  <div class="skeleton-image"></div>
  <div class="skeleton-text"></div>
  <div class="skeleton-text short"></div>
</div>

<style>
.skeleton-image {
  aspect-ratio: 16/9;
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
</style>

4. Fonty - minimalizacja FOUT

Preload fontów:

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

Font-display: optional:

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: optional;
}

Font matching z CSS Font Loading API:

document.fonts.ready.then(() => {
  document.body.classList.add('fonts-loaded');
});
/* Przed załadowaniem fontów - fallback z dopasowanymi metrykami */
body {
  font-family: 'Inter Fallback', sans-serif;
}

/* Po załadowaniu */
body.fonts-loaded {
  font-family: 'Inter', sans-serif;
}

5. Dynamiczna treść - transform zamiast reflow

/* Źle - dodawanie elementu przesuwa resztę */
.notification {
  position: relative;
}

/* Dobrze - overlay nie wpływa na layout */
.notification {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 1000;
}

/* Lub animacja z transform */
.notification {
  position: fixed;
  top: 0;
  transform: translateY(-100%);
  transition: transform 0.3s;
}
.notification.visible {
  transform: translateY(0);
}

6. Lazy loading z placeholder

<div class="lazy-container" style="aspect-ratio: 16/9;">
  <img
    src="placeholder.jpg"
    data-src="actual-image.jpg"
    loading="lazy"
    alt="Opis"
  >
</div>
// Intersection Observer dla lazy loading
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      observer.unobserve(img);
    }
  });
});

document.querySelectorAll('img[data-src]').forEach(img => {
  observer.observe(img);
});

7. Unikanie document.write()

// Źle - document.write blokuje parser
document.write('<script src="ad.js"></script>');

// Dobrze - dynamiczne wstawianie
const script = document.createElement('script');
script.src = 'ad.js';
script.async = true;
document.body.appendChild(script);

Debugowanie konkretnych scenariuszy

Scenariusz 1: CLS z Google Fonts

Problem: Tekst zmienia rozmiar po załadowaniu fontu.

Diagnoza:

  1. Otwórz DevTools → Network
  2. Filtruj po “font”
  3. Sprawdź timing fontów vs FCP

Rozwiązanie:

<!-- Preconnect do Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

<!-- Lub self-hosting -->
<link rel="preload" href="/fonts/roboto.woff2" as="font" type="font/woff2" crossorigin>

Scenariusz 2: CLS z lazy-loaded images

Problem: Obrazy poniżej fold powodują CLS podczas scrollowania.

Diagnoza:

  1. Włącz Layout Shift Regions w Rendering
  2. Scrolluj stronę
  3. Obserwuj niebieskie prostokąty

Rozwiązanie:

<!-- Zawsze określaj wymiary -->
<img
  src="photo.jpg"
  loading="lazy"
  width="400"
  height="300"
  alt="Zdjęcie"
>

Scenariusz 3: CLS z dynamicznymi banerami

Problem: Baner cookie/notyfikacja przesuwa treść.

Diagnoza:

  1. Odśwież stronę
  2. Obserwuj moment pojawienia się banera
  3. Sprawdź czy treść się przesuwa

Rozwiązanie:

/* Baner na dole jako overlay */
.cookie-banner {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 9999;
}

/* Lub rezerwacja miejsca na górze */
.page-wrapper {
  padding-top: 60px; /* Wysokość banera */
}

Scenariusz 4: CLS z iframes (embedy)

Problem: YouTube embed, mapy, widgety społecznościowe.

Rozwiązanie:

<div class="video-container" style="aspect-ratio: 16/9;">
  <iframe
    src="https://www.youtube.com/embed/VIDEO_ID"
    loading="lazy"
    style="width: 100%; height: 100%;"
    frameborder="0"
    allowfullscreen
  ></iframe>
</div>

Checklist naprawy CLS

Obrazy i media

  • Wszystkie <img> mają width i height
  • Używam aspect-ratio dla responsywnych obrazów
  • Iframes mają zdefiniowane wymiary
  • Video posters są załadowane

Fonty

  • Fonty są preloadowane
  • Używam font-display: swap lub optional
  • Fallback font ma podobne metryki

Reklamy i embedy

  • Sloty reklamowe mają min-height
  • Embedy społecznościowe mają placeholder
  • Lazy-loaded content ma zarezerwowane miejsce

Dynamiczna treść

  • Banery używają position: fixed
  • Notyfikacje nie przesuwają treści
  • Animacje używają transform zamiast reflow

JavaScript

  • Brak document.write()
  • Dynamicznie dodawana treść ma placeholder
  • Skeleton screens dla ładowanych komponentów

Podsumowanie

CLS to metryka, która bezpośrednio wpływa na doświadczenie użytkownika. Nieoczekiwane przesunięcia są frustrujące i mogą prowadzić do przypadkowych kliknięć. Kluczowe zasady:

  1. Zawsze określaj wymiary obrazów i mediów
  2. Rezerwuj miejsce dla dynamicznej treści
  3. Optymalizuj fonty - preload i font-display
  4. Używaj position: fixed dla overlayów
  5. Monitoruj CLS w rzeczywistych danych (CrUX)

Cel to CLS poniżej 0.1 dla 75% użytkowników. Regularne testowanie i debugowanie pomoże utrzymać stabilny layout.

Źródła

  1. web.dev - Cumulative Layout Shift (CLS) https://web.dev/articles/cls

  2. web.dev - Optimize CLS https://web.dev/articles/optimize-cls

  3. web.dev - Debug layout shifts https://web.dev/articles/debug-layout-shifts

  4. Chrome Developers - Layout Instability API https://developer.chrome.com/docs/web-platform/layout-instability-api

  5. MDN - CSS aspect-ratio https://developer.mozilla.org/en-US/docs/Web/CSS/aspect-ratio