CLS (Cumulative Layout Shift) — jak naprawić w 2026
CLS (Cumulative Layout Shift) mierzy, jak bardzo elementy strony „skaczą" podczas ładowania — i w audytach, które robimy, to zwykle pierwsza rzecz, którą pokazujemy klientowi na żywo, bo nie trzeba tłumaczyć, o co chodzi: klik w przycisk, przycisk ucieka pod kursorem, klient klika coś zupełnie innego. Cel <0.1. Wysokie CLS to nie tylko frustracja użytkownika klikającego złe elementy — to też ranking penalty. W 2026 dobrze zoptymalizowane CLS bywa różnicą między top 3 a top 10 dla konkurencyjnych fraz.
TL;DR — CLS
| Metric | Detale |
|---|---|
| Co mierzy | Visual stability — element movement during load |
| Granice | Good <0.1, NI 0.1-0.25, Poor >0.25 |
| Główne przyczyny | Brak dimensions, ads, font swap, dynamic treść |
| Quick fix #1 | Width + height na wszystkich obrazach |
| Quick fix #2 | Reserve space dla ads + dynamic treść |
| Quick fix #3 | Font-display: optional or fallback |
| Tools | DevTools Skuteczność, PageSpeed Wnioski, Web Vitals |
Czym jest CLS?
Cumulative Layout Shift mierzy unexpected layout shifts during page load. To metryka, którą najłatwiej pokazać nietechnicznemu klientowi — wystarczy nagranie ekranu, na którym widać, jak treść skacze w trakcie ładowania, i tłumaczenia przestają być potrzebne. Każdy shift dostaje score na podstawie:
- Impact fraction — jak bardzo viewport affected
- Distance fraction — jak daleko element się przesunął
CLS = sum wszystkich layout shifts w session.
Granice
- Good: <0.1 (passing)
- Needs Improvement: 0.1-0.25
- Poor: >0.25
Dlaczego CLS ma znaczenie
Traktujemy to jako łańcuch, nie listę osobnych efektów — jedno prowadzi do drugiego:
- UX issue — user clicks button → button przeskakuje → clicks wrong thing
- Trust issue — site czuje się broken / amateur
- Ranking factor — Core Web Vitals signal
- Konwersja impact — frustrated users abandon
Główne przyczyny CLS
W praktyce wracają właściwie zawsze te same sześć przyczyn. Różnica między stronami klientów, których audytujemy, nie polega na tym, że pojawia się coś nowego — polega na tym, która z tej szóstki dominuje.
1. Obrazy bez dimensions
<!-- BAD — browser nie wie size, ładowanie shifts layout -->
<img src="image.jpg" alt="..." />
<!-- GOOD — browser reserves space -->
<img src="image.jpg" alt="..." width="1200" height="630" />
Fix: zawsze podaj width + height. Browser kalkuluje aspect ratio i reserves space. To przyczyna, którą widzimy najczęściej — i jednocześnie najłatwiejsza do naprawienia, bo nie wymaga przebudowy niczego, tylko dwóch atrybutów.
2. Ads injection
Ads pojawiające się po scroll injectują into existing layout, pushing treść down:
<!-- BAD — ad placeholder bez height -->
<div class="ad-slot">
<!-- Ad loads here, pushes content -->
</div>
<!-- GOOD — reserved space -->
<div class="ad-slot" style="min-height: 250px">
<!-- Ad loads, no shift -->
</div>
Fix: reserve minimum height dla każdego ad slot. Klasyczny przypadek: ktoś dodał slot reklamowy, ale nikt nie pomyślał, ile miejsca zajmie reklama, zanim się załaduje.
3. Web fonts loading
FOUT (Flash of Unstyled Text) → custom font loads → text reflows → layout shifts.
/* BAD - block render until font loaded */
@font-face {
font-family: 'CustomFont';
src: url('/font.woff2');
}
/* GOOD - prevent shift */
@font-face {
font-family: 'CustomFont';
src: url('/font.woff2');
font-display: optional; /* nie blokuje, nie shifts */
}
/* Alternative - swap fast */
@font-face {
font-display: swap; /* shows fallback, swap when loaded */
size-adjust: 95%; /* fallback similar size do custom */
}
Fix: font-display: optional (best for CLS) lub swap z size-adjust. To jedna z tych rzeczy, które klient rzadko zauważa sam — dopóki nie pokażemy, ile milimetrów tekst przeskakuje przy zmianie fontu.
4. Dynamic treść injection
Cookie banners, popups, chat widgets pojawiające się po opóźnieniu push treść:
/* GOOD - cookie banner z fixed positioning */
.cookie-banner {
position: fixed;
bottom: 0;
/* nie shifts main layout */
}
Fix: dynamic treść jako overlay, nie inline.
5. Embed iframes
YouTube, Twitter, social embeds — często bez aspect ratio:
<!-- BAD - height not set -->
<iframe src="https://youtube.com/..." />
<!-- GOOD - aspect ratio container -->
<div style="aspect-ratio: 16/9;">
<iframe src="https://youtube.com/..." style="width: 100%; height: 100%;" />
</div>
Fix: wrap embed w aspect-ratio container.
6. Lazy-loaded sections
Treść appearing on scroll może shifts layout if not properly positioned.
<!-- BAD - lazy section appears, pushes below -->
<section class="lazy-load">
<!-- Content loads here -->
</section>
<!-- GOOD - reserved height -->
<section class="lazy-load" style="min-height: 600px">
<!-- Content loads, no shift -->
</section>
Debugging CLS
Diagnozę zaczynamy zawsze od tego samego pytania: gdzie i kiedy dokładnie strona się rusza. Bez konkretnego zrzutu shiftu naprawiasz na oślep.
Chrome DevTools
- F12 → Skuteczność tab
- Settings → Show layout shifts
- Click record
- Refresh page
- Stop recording
- Layout shifts highlighted (red rectangles)
PageSpeed Wnioski
pagespeed.web.dev → CLS section pokazuje:
- CLS score
- Largest contentful shifts
- Recommendations
Web Vitals Chrome Extension
Real-time CLS w toolbar. Test live as you browse.
Lighthouse
Lighthouse CI z CLS threshold w pipeline (fail builds <0.1).
Fixes per common scenario
Każdy z poniższych scenariuszy to konkretny przypadek, z którym się mierzyliśmy — nie teoretyczna lista z dokumentacji.
Scenario 1: Hero image LCP causing CLS
<!-- Without width/height, image loads → shifts everything below -->
<img src="hero.jpg" alt="Hero" />
<!-- Fix -->
<img src="hero.jpg" alt="Hero" width="1920" height="800"
style="width: 100%; height: auto;" />
CSS keeps responsive size while preserving aspect ratio.
Scenario 2: WordPress dynamic widgets
WordPress widgets często load asynchronously:
- Newsletter signup widget loads → shifts
- Related posts plugin → shifts
Fix:
.widget-area {
min-height: 400px; /* reserved */
}
Scenario 3: React hydration shift
React SSR + hydration sometimes shifts layout:
- Server renders A
- Client hydrates → component re-renders B
- Shift!
Fix:
- Match SSR + CSR (avoid client-only logic w initial render)
- Use
suppressHydrationWarningcarefully - Skeleton screens dla loading states
Scenario 4: Video embeds (YouTube, Vimeo)
<!-- Aspect-ratio container -->
<div style="aspect-ratio: 16/9; max-width: 100%;">
<iframe src="https://youtube.com/embed/..."
style="width: 100%; height: 100%; border: 0;" />
</div>
Scenario 5: Google Ads / banner ads
Reserve dla ad slots:
<div class="ad-slot" style="min-height: 250px; min-width: 300px;">
<!-- AdSense, GAM, etc. -->
</div>
Use Google Ad Manager fixed-size slots zamiast responsive.
CSS techniques
aspect-ratio (modern, recommended)
To technika, którą wybieramy domyślnie, jeśli tylko wsparcie przeglądarek na to pozwala:
.image-container {
aspect-ratio: 16/9;
width: 100%;
}
Padding-bottom hack (legacy)
Sięgamy po to tylko tam, gdzie trzeba wspierać naprawdę stare przeglądarki:
.image-container {
position: relative;
padding-bottom: 56.25%; /* 16:9 aspect */
}
.image-container img {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
}
Min-height reserves
.section-with-dynamic-content {
min-height: 400px;
}
Mobile vs desktop CLS
Mobile typowo wyższe CLS:
- Smaller viewport — same shift bigger %
- Slower fonts/images load
- Touch interactions more shift-sensitive
Test priority: mobile-first dla CLS optimization. Skoro Google i tak indeksuje mobile-first, testowanie CLS na desktopie i uznawanie sprawy za zamkniętą to najczęstszy błąd, jaki widzimy.
CLS dla różnych frameworków
Stack, na którym stoi strona klienta, zawęża od razu listę podejrzanych — i listę fixów.
Next.js / React
<Image />component automatic dimensionsfontFamilyznext/font— automatic font-display swapnext/scriptzlazyOnload
Astro
- Built-in image optimization
- Component islands minimize hydration shift
WordPress
- Lazy load images native (5.5+)
- Plugin: WP Rocket — lazy + fixes
- Theme z proper image dimensions critical
Vanilla HTML
- Manual width/height
- CSS aspect-ratio
- Font-display: optional/swap
Najczęstsze błędy
- Brak width/height na obrazach — top przyczyna
- Custom fonts bez font-display — FOUT shifts
- Ads bez reserved space
- Lazy-loaded treść bez height
- Dynamic JS injecting treść synchronously
- YouTube embeds bez aspect-ratio
- Cookie banner inline zamiast overlay
- Test tylko na fast WiFi — slow connections show CLS more
CLS optimization checklist
- Wszystkie obrazy mają width + height
- Font-display: optional / swap
- Ads slots mają min-height
- Iframes z aspect-ratio
- Cookie banner = overlay (fixed position)
- Lazy-loaded sections z reserved space
- Test mobile + desktop
- Test slow 3G connection
- CLS w real user monitoring (Web Vitals JS)
- CLS w CI/CD pipeline (fail <0.1)
Mierzenie + monitoring
Lab data z Lighthouse to punkt startowy, nie dowód. Dopiero dane z realnych użytkowników pokazują, czy naprawa faktycznie zadziałała.
Real User Monitoring (RUM)
import {onCLS} from 'web-vitals';
onCLS(metric => {
console.log('CLS:', metric.value);
// Send to analytics
gtag('event', 'web_vitals', {
name: 'CLS',
value: metric.value,
});
});
Track real users, identify problem pages.
CrUX data
PageSpeed Wnioski — realne dane CrUX (pole) Twojej strony vs próg CLS.
Podsumowanie
CLS 2026:
- Cel <0.1 dla passing CWV
- Width + height na wszystkich obrazach
- Font-display: optional lub swap z size-adjust
- Reserve space dla ads + dynamic treść
- Aspect-ratio CSS dla embeds
- Test mobile primarily
- RUM + CI dla continuous monitoring
Z tego, co widzimy w audytach, CLS to najłatwiejszy do naprawienia Core Web Vital — a mimo to najdłużej ignorowany, bo nikt nie czuje go tak boleśnie jak wolnego LCP. Większość poprawek to kilka atrybutów i jedna linijka CSS, nie przebudowa strony. Kilka godzin pracy zwykle wystarcza, żeby przejść z „poor" na passing Page Experience score.
Audyt Core Web Vitals + CLS — sprawdzimy obecne CLS + roadmap fixes.
Chcesz osiągnąć podobne rezultaty? Zamów bezpłatną wycenę.