CLS optimalizálás 2026: a Cumulative Layout Shift csökkentése a gyakorlatban

A Cumulative Layout Shift (CLS) csökkentésének gyakorlati útmutatója 2026-ra: aspect-ratio, font-display: optional, content-visibility és RUM mérés web-vitals.js v4-gyel. Valós kódpéldák képekhez, fontokhoz és SSR streaminghez.

CLS optimalizálás 2026: gyakorlati fix

Frissítve: 2026. június 27.

A CLS optimalizálás 2026 egy egyszerű alapelven nyugszik: minden vizuális elemnek előre lefoglalt helye kell legyen, mielőtt a böngésző megfesti. A Cumulative Layout Shift (CLS) azt méri, mennyit ugrál a látható tartalom a betöltés és a felhasználói interakciók során. A Google szerint a "jó" érték 0,1 alatt van, a p75 percentilis CrUX adatok alapján. Backend szemszögből nézve a CLS gyakran nem CSS-probléma, hanem cache- és streaming-stratégia kérdése. Ha a HTML először size hint nélkül érkezik, a kliens kénytelen találgatni, és találgatni mindig drága.

  • A CLS küszöbértékek 2026-ban változatlanok: jó < 0,1, javításra szorul 0,1–0,25, gyenge > 0,25 a session window aggregálással mérve.
  • A layout shift fő okai: méret nélküli képek és iframe-ek, késleltetett betöltésű webfontok (FOIT/FOUT), dinamikusan beszúrt bannerek és later érkező hirdetések.
  • Az aspect-ratio CSS-tulajdonság és a width/height HTML-attribútumok együtt megakadályozzák a képek miatti elmozdulást.
  • A font-display: optional és preloadolt WOFF2 fájlok minimalizálják a webfont okozta CLS-t mobilon.
  • Az új content-visibility: auto és contain-intrinsic-size kombináció off-screen elemeknél is stabilan tartja a layoutot.
  • RUM (web-vitals.js v4) használata kötelező, mert a labor mérés (Lighthouse) elrejti a felhasználói interakciók okozta shifteket.

Mi az a Cumulative Layout Shift?

A Cumulative Layout Shift egy felhasználó-központú Core Web Vital metrika, ami a vizuális stabilitást méri. Konkrétan azt összegzi, hogy egy oldalon a látható elemek mennyit mozdultak el váratlanul (azaz nem felhasználói interakció hatására) egy úgynevezett session window alatt. Egymást követő shiftek 1 másodperces különbséggel, max. 5 másodperces ablakban. 2020 óta a Google a legnagyobb ilyen ablak értékét veszi figyelembe, és ez a változás kifejezetten a single-page application-öket érintette, ahol egy hosszú scrollozási session korábban indokolatlanul felpumpálta a CLS-t.

A metrika értékét két tényező szorzata adja: az impact fraction (mekkora terület érintett a viewport-hoz képest) és a distance fraction (milyen messzire mozdult az elem). Egy 50%-os impact és 30%-os distance shift például 0,15 layout shift score-t generál, és ez egyetlen ilyen esemény, ami már átlépi a "jó" küszöböt. A web.dev hivatalos CLS dokumentációja részletesen tárgyalja a számítási logikát.

Saját tapasztalatom szerint a legtöbb csapat azzal a téves hittel kezdi a CLS-t javítani, hogy ez egy "Lighthouse-szám", amit egyszer letudunk. Valójában a CLS folyamatosan keletkezik a felhasználói munkamenet alatt: scrollozás közben tölthető tartalom, interakcióra megnyíló accordion, dinamikus ár- vagy stock-info. Mindezek hozzájárulnak. Ezért a CLS optimalizálás 2026-ban már nem build-time feladat, hanem RUM-monitorozott, folyamatosan finomított munkafolyamat.

Hogyan mérjük a CLS-t 2026-ban?

Háromféle mérési forrást használj kombinálva: Lighthouse (labor), CrUX (real-world p75) és saját RUM web-vitals.js-szel. A web-vitals.js v4 GitHub repository standard API-t biztosít minden Core Web Vital eseményhez, beleértve a CLS attributiont is, ami megmondja, hogy konkrétan melyik DOM-elem okozta a shiftet.

A laborhoz a Chrome DevTools Performance panel "Layout shifts" sora vizuálisan kiemeli a problémás elemeket: pirossal jelölve a "before" pozíciót és kékkel az "after"-t. Az olyan eszközök, mint a PageSpeed Insights vagy a WebPageTest CrUX-integrációval rendelkeznek, így a 28 napos p75 mediánt is láthatod. Egy minimalista RUM beépítés így néz ki:

// app/layout.tsx vagy entry pont
import { onCLS } from 'web-vitals/attribution';

onCLS((metric) => {
  // metric.attribution.largestShiftTarget = 'div#hero-banner'
  // metric.attribution.largestShiftValue = 0.087
  // metric.attribution.loadState = 'dom-content-loaded'
  navigator.sendBeacon('/rum', JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
    target: metric.attribution.largestShiftTarget,
    url: location.pathname,
  }));
});

A sendBeacon azért fontos, mert a CLS gyakran csak az oldal elhagyásakor véglegesedik, és az unload idején a hagyományos fetch elveszhet. A részletekért lásd a RUM és web-vitals.js gyakorlati útmutatónkat.

A layout shift leggyakoribb okai 2026-ban

Az elmúlt két évben több ezer oldal RUM-adatát átnézve a CLS-probléma 90%-a hét forrásra vezethető vissza. Ezeket fontos azonosítani, mielőtt elkezdesz javítani, különben fix-a-fix után kerget a többi forrás.

  1. Méret nélküli képek és videók, vagyis hiányzó width/height attribútum, ami miatt a böngésző 0 magasságot feltételez.
  2. Webfont swap (FOUT), ahol a fallback és a custom font különböző cap-height-tal rendelkezik.
  3. Hirdetések és embedded widgetek: YouTube, Twitter, hirdetésrendszerek, amelyek dinamikus magasságot kapnak.
  4. Dinamikusan beszúrt tartalom: cookie banner, GDPR popup, "New post" infinite scroll értesítő.
  5. Animations layout property-vel, ahol top, left, width animálása helyett transform-ot kell használni.
  6. SSR/CSR hidratációs eltérések, amikor a szerveren rendert markup eltér a kliens által várt struktúrától.
  7. Skeleton screen váltások, amikor a placeholder és valós tartalom mérete nem egyezik.

Hogyan akadályozzuk meg a képek miatti layout shiftet?

A képek miatti CLS-t a width/height attribútumok és az aspect-ratio CSS-tulajdonság kombinációja oldja meg. A modern böngészők (Chrome 88+, Firefox 89+, Safari 14+) az HTML attribútumok alapján automatikusan kiszámolják az aspect-ratio-t, így a CSS oldalon csak width: 100%; height: auto; kell. A 2026-os baseline támogatás 96% feletti minden modern böngészőben.

<!-- HELYES: a width/height meghatározza az aspect-ratio-t -->
<img src="/hero.avif"
     width="1200" height="675"
     alt="Hero kép"
     style="width: 100%; height: auto;"
     fetchpriority="high"
     decoding="async" />

<!-- HELYTELEN: méret nélkül a böngésző 0×0-val kezd, majd reflow -->
<img src="/hero.avif" alt="Hero kép" />

Reszponzív képeknél, ahol a srcset különböző méreteket szolgál ki, az aspect-ratio CSS-tulajdonság hatékonyabb:

.media-thumb {
  aspect-ratio: 16 / 9;
  width: 100%;
  background: #eee;
  object-fit: cover;
}

Az iframe-ek (YouTube embed, Twitter, hirdetések) ugyanezt a logikát követik, de itt a "facade pattern" hozzáadása dupla előnyt jelent, mert nemcsak a CLS-t, hanem az LCP-t és INP-t is csökkenti. A részletekért nézd meg a képoptimalizálási útmutatónkat, ami az AVIF/WebP konverziót és a srcset stratégiát is tárgyalja. A MDN aspect-ratio referenciája a böngészőfüggő szegélyeseteket is dokumentálja.

Webfontok és a font-display stratégia

A webfont okozta CLS-t (gyakran "font swap shift"-nek hívják) a fallback és custom font közötti metrikabeli különbség okozza. Ha az Inter font cap-height-ja 1116 unit (UPM-ben), a system-ui pedig 1024, a swap pillanatában minden szöveg átrendeződik. Egy 600 pixeles bekezdésen ez 0,15+ CLS-t is okozhat. Én ezt utoljára egy e-kereskedelmi projekten futottam neki, ahol egyetlen Inter swap önállóan a teljes CLS budgetet elvitte.

2026-ban három technika kombinálandó a stabil CLS-hez.

1. font-display: optional

A font-display: optional érték a böngészőnek azt mondja: ha 100 ms-en belül nem érkezik meg a font, használja a fallbacket és többé ne cseréljen. Ez nullára csökkenti a swap shiftet, mert a swap soha nem történik meg az első nézetben.

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-var.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-display: optional;
  size-adjust: 100.06%;
  ascent-override: 90.2%;
  descent-override: 22.48%;
  line-gap-override: 0%;
}

2. Metric override-ok

A size-adjust, ascent-override, descent-override és line-gap-override tulajdonságok lehetővé teszik, hogy a fallback fontot szinte pixelpontosan illesszük a custom fonthoz. A Fontaine és a Capsize könyvtárak automatikusan generálják ezeket az értékeket egy tetszőleges WOFF2 fájl alapján.

3. Preload + crossorigin

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

A részletes implementációért lásd a webes betűtípus optimalizálási útmutatónkat.

Dinamikusan beszúrt tartalom kezelése

A cookie banner, GDPR popup, "subscribe to newsletter" sáv és A/B teszt overlay-ek a leggyakoribb dinamikus CLS-források. Az alapelv: soha ne tolj le tartalmat fent megjelenő bannerrel. Helyette használj fixed pozíciójú elemet alul, vagy egy modal overlay-t, ami nem mozgatja a háttér tartalmát.

/* HELYES: a banner kívül esik a normal flow-n */
.cookie-banner {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 1000;
}

/* HELYTELEN: a body-ba beszúrva lenyomja a tartalmat */
.cookie-banner-bad {
  position: relative;
  margin-bottom: 20px;
}

Hirdetésekre és A/B teszt slot-okra reserve magasságot CSS-ben:

.ad-slot {
  min-height: 250px;  /* IAB Medium Rectangle */
  display: flex;
  align-items: center;
  justify-content: center;
  background: #f5f5f5;
}

content-visibility és intrinsic size kombináció

A content-visibility: auto egy 2024-ben általánossá vált CSS-tulajdonság, ami a viewport-on kívüli elemeket "kihagyja" a layout fázisból. Önmagában azonban a böngésző 0-ra becsüli a magasságot, ami scrolling közben CLS-t okoz, ahogy a tartalom látható lesz. A megoldás a contain-intrinsic-size:

article.blog-card {
  content-visibility: auto;
  contain-intrinsic-size: auto 400px;
}

/* "auto" kulcsszó: a böngésző memorizálja a tényleges méretet */
.faq-section {
  content-visibility: auto;
  contain-intrinsic-size: auto 800px;
}

Az auto kulcsszó kritikus, mert enélkül minden viewport-újraszámolásnál visszaáll a placeholder mérete. Ez különösen hosszú blogcikkeken és terméklisztákon 30–40% rendering-időt is megspórolhat, miközben a CLS stabil marad.

SSR streaming és CLS, a backend szemszöge

Full-stack perf vezetőként számtalanszor láttam, hogy a frontend csapat órákat tölt CSS-fixekkel, miközben az igazi probléma a streamingben rejlik. A modern keretrendszerek (Next.js App Router, Remix, SvelteKit) HTML-darabokat streamelnek, és ha a kritikus layout-information (pl. méretadatok inline style-ban) később érkezik, mint az érintett DOM-elem, a kliens kétszer is layout-ot számol.

A megoldás: a kritikus CSS-t és az inline méreteket az első chunk-ban kell elküldeni, lehetőleg <head>-be inline-olva. Next.js-ben a generateMetadata és a layout-szintű CSS biztosítja ezt:

// app/products/[id]/layout.tsx
export default function ProductLayout({ children }) {
  return (
    <>
      <style>{`
        .product-hero { aspect-ratio: 4 / 3; min-height: 320px; }
        .product-grid { display: grid; grid-template-rows: 320px auto; }
      `}</style>
      {children}
    </>
  );
}

Animációk: transform vs. top/left

A CSS animációk akkor okoznak CLS-t, ha layout-property-ket animálnak: top, left, width, height, margin, padding. Ezek mindegyike reflow-t indít, ami layout shift-ként rögzítődik. A megoldás a transform és az opacity használata. Ezek a compositor thread-en futnak és NEM számítanak layout shiftnek a böngésző számára.

/* HELYTELEN: layout reflow + CLS hozzájárulás */
@keyframes slide-bad {
  from { left: -100px; }
  to   { left: 0; }
}

/* HELYES: compositor-only, nincs CLS */
@keyframes slide-good {
  from { transform: translateX(-100px); }
  to   { transform: translateX(0); }
}

.menu-panel {
  animation: slide-good 200ms ease-out;
  will-change: transform;
}

A will-change hint segíti a böngészőt, hogy előre dedikált compositor layer-t hozzon létre, így az animáció akár 60+ FPS-en futhat anélkül, hogy main thread-et terhelne. Ne használd túl sok elemre, mert minden layer memóriát foglal.

Hogyan debugolod a CLS-t Chrome DevTools-szal?

A Chrome DevTools Performance paneljén négy lépésben azonosíthatod a CLS-források mindegyikét.

  1. Nyisd meg a Performance panelt, kapcsold be a "Web Vitals" opciót.
  2. Töltsd újra az oldalt, görgess végig a tartalmon, várj 5–10 másodpercet az interakciók után is.
  3. A timeline tetején keresd a piros "Layout Shift" markereket a Web Vitals sávban.
  4. Kattints rá: az "Experience" szekció megmutatja az érintett elemet, az előtti és utáni rect-et, valamint a score-t.

Az új Performance Insights AI-asszisztens (Chrome 128+) automatikusan ajánlatot tesz fix-re, pl. "Add explicit width/height to img#hero". A parancssoros workflowhoz a Lighthouse CI is ad cls auditot, ami a CI-ban bukja meg a build-et, ha a CLS > 0,1.

# .lighthouserc.json
{
  "ci": {
    "assert": {
      "assertions": {
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }]
      }
    }
  }
}

Ha kíváncsi vagy az INP és CLS együttes mérésére interakciókkal terhelt oldalakon, nézd meg az INP optimalizálási útmutatónkat is. Tapasztalatom szerint a két metrika gyakran ugyanattól a problémától romlik (long task által késleltetett render), de a fix-ek különböznek. Érdemes mindkettőt párhuzamosan figyelni a RUM dashboardon.

Gyakran ismételt kérdések

Mi a jó CLS érték 2026-ban?

A "jó" CLS küszöb 0,1 alatt van, a "javításra szorul" 0,1 és 0,25 között, és a "gyenge" 0,25 felett. Ezt a p75 percentilisre kell mérni a CrUX vagy saját RUM adataidban, nem az átlagra. A küszöbök 2020 óta változatlanok és 2026-ban sem terveznek módosítást.

Miért rossz a CLS-em, ha Lighthouse-ban 0?

A Lighthouse csak az oldal betöltését szimulálja headless módban, interakciók nélkül. A valódi CLS nagy része user interaction utáni dinamikus tartalomból (modal, accordion, "Load more" gomb) származik, amit csak RUM web-vitals.js-szel mérsz a valódi user session-ökön.

A lazy-loaded képek CLS-t okoznak?

Csak akkor, ha nincs explicit width/height vagy aspect-ratio megadva. A loading="lazy" önmagában nem ok, hanem a méretinformáció hiánya az. Mindig kombináld a lazy loadingot intrinsic size hint-tel.

Hogyan csökkenthetem a CLS-t React/Next.js-ben?

A Next.js <Image> komponens automatikusan generálja a width/height-ot és reservál layout helyet. Kerüld a kliens oldali fetch utáni renderelést hero szekcióban, és használj Server Componenseket. A font preloadot a next/font kezeli automatikusan, beleértve a metric override-okat is.

Számít-e a sütibanner a CLS-be?

Igen, ha az oldalbetöltés első 5 másodpercében jelenik meg és lenyomja a tartalmat. Megoldás: position: fixed bottom banner vagy modal overlay, ami nem mozgatja a háttér tartalmat. A felhasználói "Elfogadom" kattintás után 500 ms-on belüli változások már nem számítanak.

Mateo Silva
A Szerzőről Mateo Silva

Full-stack performance lead bridging frontend perf with backend latency. Cache invalidation is his love language.