Przeskok układu z jednego okna 5 sekund: od wartości CLS do węzła DOM, który ją wywołał
CLS to wynik największego okna sesji, nie suma przesunięć. Jak dojść od liczby do konkretnego elementu w DOM i czym rezerwować dla niego miejsce w kodzie.
Nagłówek zjeżdża o 300 pikseli w dół, bo obrazek nad nim dostał w końcu wysokość. Czytelnik sięgał w tej chwili po link i trafił w inny. Jedno takie zdarzenie, przeliczone na metrykę, potrafi samo wyrzucić stronę powyżej 0,25 – progu, który dokumentacja CLS na web.dev oznacza jako wynik słaby.
Przeskok układu ma przy tym własność, której nie mają dwie pozostałe metryki Core Web Vitals: rozkłada się na pojedyncze zdarzenia aż do konkretnego węzła DOM, i to również na urządzeniu użytkownika. Kosztuje to kilkanaście linii kodu. Trudność leży indziej – w kodzie z generatora przeskok ma zwykle kilka źródeł naraz, a usunięcie jednego z nich przenosi winę na następne.
Co dokładnie liczy CLS i dlaczego to jedna liczba z całej sesji
CLS nie jest sumą wszystkich przesunięć na stronie. Jak opisuje dokumentacja metryki na web.dev, przesunięcia grupowane są w okna sesji: okno to seria przesunięć, między którymi mija mniej niż sekunda, i które w całości trwa najwyżej 5 sekund. CLS strony to wynik największego z tych okien, nie suma wszystkich.
Pojedyncze przesunięcie wycenia się jako iloczyn dwóch ułamków. Pierwszy to udział obszaru, który się ruszył: powierzchnia niestabilnych elementów z poprzedniej i obecnej klatki podzielona przez powierzchnię okna. Drugi to największy dystans, jaki przebył którykolwiek z tych elementów, podzielony przez dłuższy wymiar okna. Stąd wniosek praktyczny: jeden skok nagłówka o 300 pikseli na pierwszym ekranie waży więcej niż dziesięć drgnięć stopki o 4 piksele.
Dla progów liczy się 75. percentyl wejść, oddzielnie dla urządzeń mobilnych i desktopów. Nie mediana – czyli strona, która u trzech na cztery osoby jest stabilna, wciąż może nie przechodzić.
| CLS na 75. percentylu | Ocena | Co to znaczy przy odbiorze |
|---|---|---|
| 0,1 i mniej | dobra | Jest co wpisać do protokołu odbioru. |
| powyżej 0,1 do 0,25 | wymaga poprawy | Zwykle jeden winowajca, nie przebudowa. |
| powyżej 0,25 | słaba | Coś dużego rusza się nad zgięciem strony. |
Jest jeszcze wyjątek, który bywa mylnie rozumiany jako furtka. Przesunięcia w ciągu 500 ms od interakcji dostają flagę hadRecentInput i można je pominąć – ale tylko po wejściu dyskretnym: dotknięciu, kliknięciu albo naciśnięciu klawisza. Przewijanie, przeciąganie i rozsuwanie palcami do tej listy nie należą. Dlatego zmiana układu, która zachodzi w trakcie przewijania, liczy się w całości.
Ten sam adres, dwie różne wartości: skąd bierze się różnica
Pomiar lokalny obejmuje jedno wejście, jeden viewport i zero interakcji, a okno zbierania zamyka się wraz z ładowaniem. Dane z pola liczone są przez cały czas życia strony, co dokumentacja optymalizacji CLS wskazuje jako pierwszą przyczynę rozbieżności między tymi dwiema liczbami. Przesunięcie, które zachodzi w dziesiątej sekundzie wizyty, mieści się tylko w jednej z nich.
Druga przyczyna to zakres danych. Raport w PageSpeed Insights potrafi pokazywać wartości dla całej domeny, nie dla sprawdzanego adresu. Strona produktowa dziedziczy wtedy wynik strony głównej z karuzelą i trzema banerami. Sprawdź w nagłówku sekcji, czy dane są dla URL-a, czy dla origin – zanim zaczniesz przeglądać CSS.
Trzecia jest najbardziej nieprzyjemna: przesunięć wewnątrz elementów iframe nie widać z poziomu API przeglądarki, ale trafiają one do danych pola. Osadzona mapa, odtwarzacz wideo i widget rezerwacji mogą więc odpowiadać za wynik, którego żaden skrypt na stronie nadrzędnej nie zarejestruje.
Do tego dochodzi warstwa sprzętowa. Pole to w dużej części telefony na wolnym łączu, gdzie font zamienia się po pierwszym malowaniu tekstu, a treść doładowywana leniwie wjeżdża w kadr w innym momencie niż na szybkim komputerze.
Czterech winowajców, których model wstawia seryjnie
Skala tych czterech problemów jest policzona na całej sieci. Według rozdziału o wydajności w Web Almanac 2025, opartego na danych z lipca 2025, dobry wynik CLS osiąga 81 proc. stron mobilnych i 72 proc. desktopowych. Czerwony pasek nie jest więc stanem naturalnym, z którym klient musi się pogodzić – to mniejszość.
Ten sam rozdział pokazuje, co leży u źródła. Brak wymiarów przy obrazkach dotyczy, według tych samych danych, 62 proc. stron mobilnych i 65 proc. desktopowych, przy medianie dwóch obrazków bez wymiarów na stronę i około 23-26 na dziewiątym decylu. Istotniejszy jest brak wysokości niż szerokości, bo strony przewija się w pionie. Fontów z sieci używa 87 proc. badanych stron mobilnych, a podpowiedzi zasobów są rzadkie: preload występuje na 15-16 proc. stron. Animacje właściwości wymuszających układ zarejestrowano na 40 proc. stron mobilnych i 44 proc. desktopowych.
| Źródło przeskoku | Skala w polu (2025) | Objaw na ekranie | Pierwszy ruch |
|---|---|---|---|
| Obrazek bez wymiarów | 62 proc. stron mobilnych | Tekst zjeżdża w dół, gdy obrazek się domaluje | Atrybuty width i height w znaczniku |
| Font z sieci | 87 proc. stron używa co najmniej jednego | Akapit zmienia liczbę linii po zamianie kroju | font-display i metryki zapasowego kroju |
| Treść wstrzykiwana późno | nie zliczona wprost, widoczna po załadowaniu | Baner zgód albo slot reklamowy rozpycha stronę | Rezerwacja miejsca albo warstwa nad treścią |
| Animacja wymuszająca układ | 40 proc. stron mobilnych | Sąsiedzi animowanego elementu drgają | Przejście na transform |
Od wartości do węzła DOM: atrybucja przesunięcia
Element przesunięty i element odpowiedzialny za przesunięcie to dwie różne rzeczy, a listy wygenerowane automatycznie pokazują zwykle pierwszy z nich. Akapit zjeżdża, bo zmienił rozmiar obrazek nad nim – naprawa dotyczy obrazka, nie akapitu. Panel wydajności w narzędziach deweloperskich Chrome ma na to osobną ścieżkę przesunięć wraz ze wskazaniem prawdopodobnych sprawców i to jest punkt startowy.
Lokalnie najszybciej działa obserwator wklejony do konsoli. Pokazuje wartość każdego przesunięcia i węzły, które je wywołały:
new PerformanceObserver((lista) => {
for (const wpis of lista.getEntries()) {
if (wpis.hadRecentInput) continue;
console.log(wpis.value, wpis.sources.map((zrodlo) => zrodlo.node));
}
}).observe({type: 'layout-shift', buffered: true});
To jednak nadal lab – twoje łącze, twój telefon, twoja ścieżka po stronie. Żeby zobaczyć pole, zostaw w projekcie bibliotekę web-vitals w wariancie z atrybucją. Przewodnik po debugowaniu wydajności w polu pokazuje dokładnie ten wzorzec: wartość metryki plus selektor elementu z największego przesunięcia, wysłane do analityki jako osobny parametr.
import {onCLS} from 'web-vitals/attribution';
onCLS(({value, attribution}) => {
wyslijZdarzenie('CLS', {
metric_value: value,
debug_target: attribution.largestShiftTarget,
sciezka: location.pathname,
});
});
Sens tego zabiegu jest statystyczny. U jednej osoby winowajcą będzie baner zgód, u innej doładowana lista opinii. Dopiero zestawienie selektorów po wszystkich wejściach mówi, który element psuje wynik największej liczbie osób – i tylko ten warto ruszać pierwszy. Ścieżka URL-a w zdarzeniu jest konieczna w aplikacjach jednostronicowych, gdzie adres zmienia się bez przeładowania.
Rezerwacja miejsca w kodzie: obrazy, font, wstrzykiwany interfejs
Obrazki są najtańszą poprawką w całej liście. Atrybuty width i height wystarczą, żeby przeglądarka wyliczyła z nich proporcje i zostawiła miejsce przed pobraniem pliku. Elastyczność dokłada się w CSS:
<!-- model generuje najczęściej to: -->
<img src="/hero.webp" alt="Panel zamówień">
<!-- a potrzebne jest to: -->
<img src="/hero.webp" alt="Panel zamówień" width="1600" height="900">
img { width: 100%; height: auto; }
Przy srcset i picture proporcje muszą być zgodne między wariantami, a wymiary trafiają również na znaczniki source. Dla slotów o nieznanej z góry treści – reklama, osadzony widget, blok rekomendacji – miejsce rezerwuje się przez aspect-ratio albo min-height, ustawione na najmniejszy przewidywany rozmiar. Zwijanie takiego miejsca, gdy nic nie przyszło, też jest przesunięciem, więc placeholder zostaje. Treść doładowywaną najlepiej umieścić nisko, bo przesunięcie nad zgięciem strony kosztuje w metryce wielokrotnie więcej.
Baner zgód i zapis na newsletter mają inne rozwiązanie: warstwa nad treścią, wyjęta z normalnego przepływu dokumentu. Nic wtedy nie rozpycha strony. Przy liście „pokaż więcej” wymiana treści zachodzi w kontenerze o ustalonej wysokości, a akcję uruchamia użytkownik, nie timer. Animacje przestawia się z top, left i box-shadow na transform, który nie przelicza układu.
Font jest przypadkiem najtrudniejszym, bo przeskok następuje przy zamianie kroju zapasowego na docelowy. Opcje są dwie. Pierwsza to font-display: optional: krój z sieci zostanie użyty tylko wtedy, gdy zdąży przed pierwszym układem, więc ponownego przeliczenia nie będzie. Druga to zbliżenie metryk kroju zapasowego do docelowego. Deskryptor size-adjust, zgodnie z dokumentacją MDN, przyjmuje wartość procentową z domyślną 100 proc. i skaluje wszystkie metryki kroju; obok niego działają ascent-override, descent-override i line-gap-override. Ta sama dokumentacja oznacza deskryptor jako szeroko dostępny we wszystkich głównych przeglądarkach od września 2023.
@font-face {
font-family: 'Krój docelowy';
src: url('/fonts/kroj.woff2') format('woff2');
font-display: optional;
}
@font-face {
font-family: 'Krój zapasowy dopasowany';
src: local('Arial');
size-adjust: 97%;
ascent-override: 90%;
}
body { font-family: 'Krój docelowy', 'Krój zapasowy dopasowany', sans-serif; }
Wartości procentowe w tym listingu są miejscem do podstawienia, nie stałą do przepisania – wylicza się je dla konkretnej pary krojów, porównując ich metryki. Bez tego kroku deskryptor potrafi pogorszyć wynik. Osobno zadbaj o to, by krój zapasowy był wskazany jawnie, a nie domyślny dla przeglądarki.
Kolejność pomiaru, która wyłapuje przesunięcia po załadowaniu
Jedno uruchomienie audytu pokrywa ułamek przesunięć, bo nie przewija strony, nie wraca na nią drugi raz i nie zmienia trasy. Poniższa kolejność pokrywa resztę. Jeśli ma ją wykonywać ktoś inny niż autor kodu, musi być zapisana tak, żeby dała się odtworzyć bez dopytywania – reguły spisywania procedury wykonywalnej przez inną osobę są tu te same co w każdym innym procesie w firmie:
- Sprawdź dane pola w PageSpeed Insights i ustal, czy dotyczą adresu, czy całej domeny.
- Przejdź stronę na zdławionym łączu i przewiń do samego końca – treść leniwa pokaże się tylko wtedy.
- Wejdź drugi raz, z zapamiętaną zgodą, i trzeci w oknie prywatnym. Baner zgód, który u Ciebie pojawia się raz, dla każdego nowego użytkownika pojawia się od nowa.
- Uruchom obserwator z konsoli i zapisz wartości razem z węzłami ze
sources. - Sprawdź osobno każdy element
iframe, bo tych przesunięć API nie pokaże. - W aplikacji jednostronicowej zmień trasę kilka razy, nie tylko odśwież wejście.
- Zostaw w projekcie
onCLSz atrybucją podpięte do analityki klienta. - Wpisz do protokołu wynik, viewport i datę pomiaru. Bez tych trzech danych liczba nic nie znaczy.
Ostatni punkt jest też granicą odpowiedzialności, którą opłaca się ustalić na piśmie. CLS mierzy się u użytkownika i na treści, która powstaje po odbiorze – wystarczy, że klient wstawi do wpisu obrazek bez wymiarów albo wklei skrypt czatu, i wynik się zmieni. Dlatego uczciwą konstrukcją nie jest gwarancja wartości, lecz budżet wydajnościowy w umowie plus monitoring po wdrożeniu. To drugie jest pracą powtarzalną, a nie jednorazową, i daje się rozliczać abonamentem; sam mechanizm przestawiania jednorazowych projektów na stałą obsługę jest opisany oddzielnie i wykracza poza samą metrykę.
Który przeskok naprawiać pierwszy
Reguła wynika wprost ze wzoru na wynik przesunięcia. Pierwszy w kolejce jest element duży i położony wysoko, który przesuwa się daleko – nie ten, który rusza się najczęściej. Jeśli zestawienie selektorów z pola wskazuje jeden element u większości osób, zaczynasz i kończysz na nim, a dopiero potem wracasz do listy.
Drugi wybór dotyczy poziomu poprawki. Jeden obrazek bez wymiarów naprawia się w znaczniku, dwadzieścia trzy naprawia się regułą w linterze i w bramce przed scaleniem – inaczej wrócą przy następnym wygenerowanym komponencie. To rozróżnienie decyduje, czy zamykasz zgłoszenie, czy problem. Z jednym zastrzeżeniem, które dotyczy każdej bramki, nie tylko tej: bramka, przez którą przechodzi zbyt wiele zgłoszeń, przestaje być czytana i zaczyna przepuszczać – ten sam mechanizm zalania kolejki zatwierdzeń psuje punkty kontrolne w procesach z automatyzacją. Reguła ma blokować konkretny przypadek, nie ostrzegać o wszystkim.
Na efekt trzeba poczekać. Dane pola to okno ruchome liczone w tygodniach, więc po wdrożeniu poprawki wynik nie drgnie tego samego dnia; potwierdzeniem w międzyczasie jest brak wpisów layout-shift w obserwatorze przy przejściu całej ścieżki. A jeśli pole nadal pokazuje wysoką wartość przy czystym pomiarze lokalnym, zostają dwa miejsca, w których jeszcze nie szukałeś: wnętrze elementów iframe i zmiany trasy w aplikacji jednostronicowej.