CLS (Cumulative Layout Shift) — jak naprawić w 2026
CLS (Cumulative Layout Shift) to Core Web Vital mierzące visual stability strony — jak bardzo elementy „skaczą" podczas ładowania. Cel <0.1. Wysokie CLS = frustrated users clickujący wrong elements + ranking penalty. W 2026 dobrze zoptymalizowane CLS jest często differentiator między top 3 a top 10 dla competitive queries.
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 content |
| Quick fix #1 | Width + height na wszystkich obrazach |
| Quick fix #2 | Reserve space dla ads + dynamic content |
| Quick fix #3 | Font-display: optional or fallback |
| Tools | DevTools Performance, PageSpeed Insights, Web Vitals |
Czym jest CLS?
Cumulative Layout Shift mierzy unexpected layout shifts during page load. 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
- UX issue — user clicks button → button przeskakuje → clicks wrong thing
- Trust issue — site czuje się broken / amateur
- Ranking factor — Core Web Vitals signal
- Conversion impact — frustrated users abandon
Główne przyczyny CLS
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.
2. Ads injection
Ads pojawiające się po scroll injectują into existing layout, pushing content 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.
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.
4. Dynamic content injection
Cookie banners, popups, chat widgets pojawiające się po opóźnieniu push content:
/* GOOD - cookie banner z fixed positioning */
.cookie-banner {
position: fixed;
bottom: 0;
/* nie shifts main layout */
}
Fix: dynamic content 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
Content 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
Chrome DevTools
- F12 → Performance tab
- Settings → Show layout shifts
- Click record
- Refresh page
- Stop recording
- Layout shifts highlighted (red rectangles)
PageSpeed Insights
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
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)
.image-container {
aspect-ratio: 16/9;
width: 100%;
}
Padding-bottom hack (legacy)
.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.
CLS dla różnych frameworkó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 content bez height
- Dynamic JS injecting content 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
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
CrUX Compare — CLS vs konkurencja.
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 content
- Aspect-ratio CSS dla embeds
- Test mobile primarily
- RUM + CI dla continuous monitoring
CLS to often najłatwiejszy CWV do fix — większość issues quick wins (add dimensions, font-display, reserved space). Inwestycja kilku godzin = passing Page Experience score.
Audyt Core Web Vitals + CLS — sprawdzimy obecne CLS + roadmap fixes.