Przejdź do treści
Core Web Vitals

26 proc. dobrych LCP w metryce stabilnej, 87 proc. w eksperymentalnej. Ten sam SPA, inna definicja nawigacji

CrUX publikuje już LCP, INP i CLS z miękkimi nawigacjami. Jak zmierzyć je per trasa w SPA przed oddaniem projektu i którą liczbę wpisać do odbioru.

Redakcjadeveloper front-end10 minkod + tabela
26liczba z tezy tekstu

W październikowym wydaniu CrUX serwisy, w których od 75 do 100 proc. odsłon na Androidzie to zmiany trasy po stronie klienta, mają 26 proc. udziału w kategorii „dobre LCP” liczonego bez miękkich nawigacji i 87 proc. po ich doliczeniu. Ten sam kod, ten sam ruch, dwie liczby. Różni je wyłącznie to, co przeglądarka uznaje za nawigację.

Dla kogoś, kto oddaje klientowi aplikację jednostronicową, to nie jest ciekawostka ze zmian w API. To pytanie, pod którą liczbę podpisujesz protokół odbioru, bo przez najbliższe miesiące w obiegu będą obie, w różnych narzędziach, i będą się od siebie różnić o kilkadziesiąt punktów procentowych.

Dlaczego stary pomiar karał każdą zmianę trasy

Do niedawna wszystkie trzy metryki Core Web Vitals były przypięte do jednego wczytania dokumentu. Zmiana adresu przez history.pushState() nie resetowała niczego: metryka dostawała adres, pod którym użytkownik rozpoczął wczytywanie strony, a nie ten, który faktycznie oglądał.

Konsekwencje układały się w jedną stronę, zawsze na niekorzyść aplikacji jednostronicowej:

  • LCP mierzył wyłącznie pierwsze wejście. Szósty widok, do którego użytkownik doszedł klikając, nie miał własnego LCP — nie miał go w ogóle.
  • CLS narastał przez całe życie dokumentu. Przeskoki z pięciu kolejnych widoków sumowały się w jedną wartość przypisaną do adresu wejściowego.
  • INP obejmował wszystkie interakcje w sesji, więc im dłużej ktoś korzystał z aplikacji, tym większa szansa, że jedno kliknięcie zepsuje wynik całości.

Serwis wielostronicowy dostawał czystą kartę przy każdym przejściu. Aplikacja jednostronicowa niosła cały bagaż na jednym adresie. Stąd luka 26 wobec 87 proc. — nie z tego, że te aplikacje nagle przyspieszyły.

Trzy warunki, które przeglądarka musi zobaczyć naraz

Chrome 151 wprowadził wpis PerformanceSoftNavigation. Pojawia się on, gdy zachodzą równocześnie trzy rzeczy: zaufana interakcja użytkownika, wynikający z niej widoczny contentful paint oraz zmiana adresu w pasku przeglądarki. Wykrywanie siedzi w przeglądarce, nie w aplikacji, więc nie ma znaczenia, czy framework zmienia adres na początku, czy na końcu obsługi przejścia.

Definicja jest wąska i to ona rozstrzyga, czy wygenerowany projekt dostanie sensowne dane. Dwa przypadki brzegowe, które warto sprawdzić w swoim kodzie, zanim zaufasz wynikom:

  • Fałszywe trafienia. Komponent zakładek albo filtr listy, który dopisuje parametr do adresu i przerysowuje fragment widoku, spełnia wszystkie trzy warunki. Przeglądarka policzy go jako nawigację, choć użytkownik nie zmienił strony. Generatory chętnie robią zakładki na routerze, więc liczba nawigacji w raporcie bywa wyższa niż liczba realnych widoków.
  • Przeoczenia. Przejście wywołane przekierowaniem po zalogowaniu, licznikiem czasu albo kodem bez bezpośredniej interakcji nie spełnia pierwszego warunku i nie zostanie policzone. Dokumentacja biblioteki web-vitals wymienia zarówno fałszywe trafienia, jak i przeoczenia jako znaną cechę tej heurystyki, nie jako błąd do obejścia.

Co dokładnie zeruje się przy miękkiej nawigacji

Po włączeniu raportowania miękkich nawigacji metryki przestają być kumulacją z całego dokumentu, ale każda zachowuje się inaczej. Zestawienie poniżej jest przepisane z dokumentacji biblioteki web-vitals.

Metryka Zachowanie po miękkiej nawigacji Co z tego wynika dla wygenerowanego widoku
TTFB Raportowany jako 0, nie jako czas pierwszego żądania po zmianie trasy Nie da się z niego nic wyczytać o pobieraniu danych do nowego widoku; potrzebny osobny pomiar
FCP i LCP Pierwszy i największy contentful paint po nawigacji; elementy przeniesione z poprzedniego widoku nie są kandydatami, bo nie zostały przerysowane Jeśli layout trzyma duży obraz nagłówkowy wspólny dla tras, LCP trasy policzy mniejszy element z obszaru treści
INP Licznik zerowany, obejmuje wyłącznie interakcje po nawigacji Ciężki JavaScript w samym przejściu trasy nadal wchodzi do wyniku — przesuwa się tylko to, do której trasy
CLS Zerowany, mierzony osobno od odsłony wejściowej Przeskok przy dociąganiu danych widać na tej trasie, która go powoduje, a nie na stronie startowej

Drugi wiersz tej tabeli jest najmniej oczywisty i najczęściej przestawia wnioski. Aplikacja, w której przy zmianie trasy przerysowuje się tylko kolumna treści, dostanie LCP liczony z tej kolumny. Wartość może być świetna, a użytkownik i tak czeka — bo czeka na coś, czego ta metryka nie obejmuje.

Którą liczbę zobaczy klient i w jakim narzędziu

Eksperymentalne metryki nie zastąpiły stabilnych, tylko stanęły obok nich. Stabilne wyniki Core Web Vitals pozostają niezmienione, dopóki warianty eksperymentalne są oceniane, a to oznacza dwa równoległe obiegi danych.

Co Metryka stabilna Metryka z miękkimi nawigacjami
Nazwy pól w CrUX largest_contentful_paint, interaction_to_next_paint, cumulative_layout_shift experimental_largest_contentful_paint_with_softnavs, experimental_interaction_to_next_paint_with_softnavs, experimental_cumulative_layout_shift_with_softnavs
Gdzie dostępna Wszędzie, w tym Search Console i PageSpeed Insights Zbiór BigQuery, CrUX API, CrUX History API i CrUX Vis
Przypisanie do adresu Adres, pod którym rozpoczęło się wczytywanie dokumentu Adres, do którego użytkownik przeszedł
Status Obowiązująca Eksperymentalna, może zmieniać się między wydaniami

Dwie daty, które warto mieć w kalendarzu projektu. Chrome celuje w wyjście tych metryk ze statusu eksperymentalnego w maju 2027, a Search Console przejdzie na nowe dane po tym przejściu. W listopadowym wydaniu CrUX mają dojść eksperymentalne metryki dla typów nawigacji i dla FCP. Do maja 2027 raport, który klient otwiera sam, pokazuje starą liczbę — i to ona jest podstawą rozmowy o odbiorze, nawet jeśli uważasz ją za niesprawiedliwą wobec architektury projektu. Dwie poprawne liczby dla jednego wskaźnika to nie przypadłość tej metryki — ta sama rozbieżność powstaje wszędzie, gdzie zmienia się mianownik, a nazwa wskaźnika zostaje bez zmian.

Pomiar per trasa, zanim dane z CrUX w ogóle się pojawią

CrUX nie pokaże nic dla serwisu bez wystarczającego ruchu, a świeżo oddany projekt takiego ruchu nie ma. Pomiar przed oddaniem robi się więc u siebie. Najkrótsza droga to biblioteka web-vitals — w wersji 6.0.0 lub nowszej obsługuje opcję reportSoftNavs dla onLCP, onFCP, onCLS i onINP.

import {onCLS, onINP, onLCP} from 'web-vitals';

function wyslij(metric) {
  const payload = {
    nazwa: metric.name,
    wartosc: metric.value,
    typ: metric.navigationType,
    adres: metric.navigationURL,
    nawigacja: metric.navigationId,
    interakcja: metric.navigationInteractionId,
  };
  navigator.sendBeacon('/rum', JSON.stringify(payload));
}

onLCP(wyslij, {reportSoftNavs: true});
onCLS(wyslij, {reportSoftNavs: true});
onINP(wyslij, {reportSoftNavs: true});

Trzy pola z tego ładunku decydują o tym, czy raport będzie czytelny. Dokumentacja biblioteki opisuje je wprost:

  • navigationType ma wartość 'soft-navigation' dla miękkich nawigacji. To jedyny pewny sposób odfiltrowania ich od zwykłych wczytań tego samego adresu.
  • navigationURL to adres, którego metryka dotyczy. Trzeba użyć tego pola, a nie location.href, bo metryki raportują się z opóźnieniem i w momencie wysyłki użytkownik może być już na innej trasie.
  • navigationId wiąże metrykę z konkretną nawigacją i potrafi wskazywać poprzedni adres, a navigationInteractionId — interakcję, która nawigację wywołała.

Jest jeden skutek uboczny włączenia tej opcji, który łatwo przeoczyć przy porównywaniu wyników sprzed i po zmianie. Flaga zmienia także pomiar samego wczytania wejściowego: metryki adresu początkowego są domykane w chwili pierwszej miękkiej nawigacji, co zamraża CLS i INP tej odsłony. Liczby dla strony startowej zwykle wyjdą lepiej niż wcześniej, bo przestają zbierać przeskoki z dalszej części sesji. To nie jest poprawa wydajności, tylko zmiana zakresu pomiaru, i w porównaniu „przed i po” należy to rozliczyć osobno.

Odczyt z surowych wpisów, gdy biblioteka nie wystarcza

Jeśli potrzebny jest pomiar poza progami Core Web Vitals — na przykład czas od kliknięcia w menu do pierwszej treści nowego widoku — trzeba zejść do wpisów soft-navigation i interaction-contentful-paint. Tu jest szczegół, na którym da się zbudować cały błędny raport: wpisy paintu dopasowuje się do nawigacji przez interactionId, a nie przez navigationId. Paint, który poszedł na ekran przed wywołaniem history.pushState(), nosi jeszcze poprzedni navigationId i przy dopasowaniu po tym polu wypadnie z wyniku.

let aktywnaNawigacja = null;
let kandydatLcp = null;

new PerformanceObserver((list) => {
  for (const wpis of list.getEntries()) {
    aktywnaNawigacja = wpis;
    // paint sprzed zmiany adresu trzeba dociągnąć z samego wpisu nawigacji
    kandydatLcp = wpis.getLargestInteractionContentfulPaint();
  }
}).observe({type: 'soft-navigation', buffered: true});

new PerformanceObserver((list) => {
  for (const wpis of list.getEntries()) {
    if (aktywnaNawigacja && wpis.interactionId === aktywnaNawigacja.interactionId) {
      kandydatLcp = wpis;
    }
  }
}).observe({type: 'interaction-contentful-paint', buffered: true});

Wynik domyka się przy następnej miękkiej nawigacji albo gdy document.visibilityState przechodzi w 'hidden'. Opóźnienie trasy czyta się z pola duration, które jest różnicą presentationTime i startTime, czyli czasem od początku interakcji do pikseli na ekranie — nie z samego presentationTime.

Dwa ograniczenia tego odczytu, oba udokumentowane. Pierwsze: bufor przeglądarki trzyma tylko pierwszych 50 wpisów soft-navigation, więc performance.getEntriesByType() nie nadaje się do aplikacji, w której ktoś siedzi godzinę — trzeba obserwatora. Drugie: rejestrację obserwatorów trzeba osłonić sprawdzeniem PerformanceObserver.supportedEntryTypes, bo w przeglądarce bez obsługi observe() rzuci wyjątkiem i zabierze ze sobą resztę skryptu pomiarowego.

Dane z dwóch silników nie składają się w jedną średnią

Te wpisy są dostępne wyłącznie w Chromium od wersji 151. MDN opisuje PerformanceSoftNavigation jako funkcję o ograniczonej dostępności, oznaczoną jako eksperymentalna, a dokumentacja biblioteki web-vitals dodaje, że przeglądarka bez obsługi raportuje tak jak wcześniej — nawet z włączoną flagą.

Praktyczny skutek jest nieprzyjemny: jeden strumień RUM miesza dwa różne sposoby liczenia tego samego wskaźnika. Średnia LCP z całego ruchu nie znaczy nic, bo ruch z Safari i Firefoksa wnosi do niej wartości z pierwszego wczytania, a ruch z Chrome — wartości per trasa. Segmentacja po silniku przeglądarki przestaje być dobrą praktyką i staje się warunkiem, żeby liczba cokolwiek znaczyła.

Ile ten problem realnie waży, zależy od tego, jak bardzo projekt opiera się na miękkich nawigacjach. Około 70 proc. serwisów ma takiego ruchu mniej niż 5 proc. odsłon, a powyżej połowy odsłon wychodzi 7,0 proc. serwisów na Androidzie i 10,6 proc. na desktopie; w tysiącu największych serwisów to już 35 proc. na Androidzie i 42 proc. na desktopie. Zwykła strona firmowa z osobnymi adresami nie zauważy zmiany. Panel klienta napisany na routerze — zauważy całą.

Co wpisać do protokołu odbioru

Lista do przejścia przed oddaniem projektu, w tej kolejności. Jeśli projekt przechodzi przez kilka osób w zespole, ta lista powinna wisieć pod spisaną procedurą przekazania zadania, a nie powstawać od zera przy każdym zleceniu:

  • Ustal, czy projekt w ogóle robi miękkie nawigacje: policz wpisy soft-navigation w typowej ścieżce użytkownika i porównaj z liczbą tras, przez które przeszedł. Rozbieżność w górę to zakładki i filtry udające nawigację.
  • Zmierz każdą trasę osobno, nie tylko wejściową. Trasa, do której użytkownik dochodzi po dwóch kliknięciach, w starym pomiarze nie istniała i nikt nigdy nie sprawdził jej LCP.
  • Sprawdź, co jest kandydatem na LCP na trasach wewnętrznych. Jeśli duży element nagłówkowy jest wspólny dla układu, metryka mierzy coś mniejszego, niż użytkownik widzi jako treść główną.
  • Zapisz w protokole, z której metryki pochodzą liczby i w jakiej przeglądarce były zbierane. Bez tego porównania po trzech miesiącach nie da się odtworzyć.
  • Oddzielnie zapisz progi, które klient będzie czytał w Search Console — to nadal metryka stabilna, nie ta z pliku z pomiarami.
  • Wpisz do kalendarza utrzymania przegląd na maj 2027. Jeśli metryki wyjdą wtedy z eksperymentu, raport klienta zmieni się bez żadnego wdrożenia z waszej strony.

Decyzja, którą trzeba podjąć teraz, dotyczy jednego zdania w umowie albo w protokole: czy odbiór projektu jest rozliczany z metryki, którą klient widzi dzisiaj w Search Console, czy z pomiaru per trasa, który odpowiada architekturze tego, co mu oddajesz. Oba warianty są obronialne. Niezapisanie żadnego oznacza, że przy pierwszej rozmowie o wydajności każda strona przyniesie własną liczbę i obie będą prawdziwe.