Przejdź do treści
Core Web Vitals

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.

Redakcjadeveloper front-end11 minkod + tabela
5liczba z tezy tekstu

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:

  1. Sprawdź dane pola w PageSpeed Insights i ustal, czy dotyczą adresu, czy całej domeny.
  2. Przejdź stronę na zdławionym łączu i przewiń do samego końca – treść leniwa pokaże się tylko wtedy.
  3. 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.
  4. Uruchom obserwator z konsoli i zapisz wartości razem z węzłami ze sources.
  5. Sprawdź osobno każdy element iframe, bo tych przesunięć API nie pokaże.
  6. W aplikacji jednostronicowej zmień trasę kilka razy, nie tylko odśwież wejście.
  7. Zostaw w projekcie onCLS z atrybucją podpięte do analityki klienta.
  8. 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.