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. Uper SEO Auditor

Wtyczka Uper SEO Auditor pozwala szybko sprawdzić wyniki Core Web Vitals bezpośrednio na analizowanej stronie — w tym wartość CLS:

Core Web Vitals w Uper SEO Auditor — LCP 124ms, CLS 0.032, INP 8ms

Uper wskazuje też konkretne elementy powodujące przesunięcia layoutu — na przykładzie vw.com widać footer i div z wartością CLS aż 0.44:

Problemy CLS wykryte przez Uper SEO Auditor na vw.com — footer i div z CLS 0.44

Kolejny przykład — mymotoworld.com z CLS 0.431, gdzie aż 11 elementów powoduje przesunięcia layoutu, w tym filtry, wrapper produktów i nagłówki:

Problemy CLS wykryte przez Uper SEO Auditor na mymotoworld.com — CLS 0.431 z licznymi źródłami przesunięć

Podobny problem na parts.vw.com — treść SEO (div.seoTextContent) powoduje przesunięcie o 0.36, a łącznie 10 elementów wpływa na niestabilność layoutu:

Problemy CLS wykryte przez Uper SEO Auditor na parts.vw.com — dynamicznie ładowana treść SEO jako główne źródło przesunięć

5. Lighthouse

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

# Lub w Chrome DevTools → Lighthouse

6. PageSpeed Insights

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

7. 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 });

8. 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ęć — dlatego stabilność layoutu jest jednym z elementów ocenianych podczas audytu SXO. 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.

CLS to jedna z wielu metryk wpływających na widoczność w Google - zobacz kompletny przewodnik po technologiach web i SEO, aby dowiedzieć się, jak wszystkie wskaźniki współgrają ze sobą.

Często zadawane pytania

Co powoduje przesunięcia layoutu (CLS) na stronie?

Najczęstsze przyczyny to: obrazy i iframe bez zdefiniowanych wymiarów, dynamicznie wstrzykiwane reklamy, późno ładowane czcionki webowe (FOUT) oraz elementy dodawane do DOM powyżej widocznej treści.

Jak znaleźć elementy odpowiedzialne za wysoki CLS?

Użyj Chrome DevTools: otwórz zakładkę Performance, nagraj sesję i szukaj zdarzeń "Layout Shift". Kliknięcie pokaże dokładny element, który się przesunął. Możesz też użyć Web Vitals extension do podglądu CLS w czasie rzeczywistym.

Jaki wynik CLS jest akceptowalny?

Google klasyfikuje CLS poniżej 0,1 jako dobry, 0,1-0,25 jako wymagający poprawy, a powyżej 0,25 jako słaby. Wynik mierzony jest na 75. percentylu danych rzeczywistych użytkowników (CrUX).

Jak naprawić CLS spowodowany przez reklamy?

Zarezerwuj stałą przestrzeń na reklamy za pomocą CSS min-height na kontenerze. Jeśli reklama się nie załaduje, użytkownik zobaczy puste miejsce zamiast przeskoku layoutu. Dla lazy-loaded reklam użyj aspect-ratio na kontenerze.

Ź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