Zacznij od danych z pola, nie od Lighthouse
Lighthouse symuluje jedno urządzenie, jedno łącze i jedno wczytanie strony bez rozszerzeń. CrUX zbiera to, co faktycznie zdarzyło się użytkownikom Chrome przez ostatnie 28 dni, i raportuje 75. percentyl. Te dwie liczby potrafią różnić się dwukrotnie i obie są prawdziwe, tylko odpowiadają na inne pytania. Decyzje podejmuj na danych z CrUX, a Lighthouse traktuj jako narzędzie do diagnozy pojedynczej zmiany.
Do bieżącego podglądu używamy API PageSpeed Insights z harmonogramem dziennym dla dwudziestu najważniejszych szablonów strony i zapisujemy wyniki do arkusza. Do głębszej diagnozy wpinamy bibliotekę web-vitals w wariancie z atrybucją, która zwraca nie tylko wartość metryki, ale też element odpowiedzialny za LCP i nazwę skryptu blokującego wątek. Bez atrybucji dyskusja o INP sprowadza się do zgadywania.
W Next.js hook useReportWebVitals wysyła te dane własnym endpointem. Wystarczy prosty odbiornik zapisujący do bazy analitycznej oraz podział po szablonie strony i typie urządzenia. Po dwóch tygodniach masz obraz, którego nie da ci żaden zewnętrzny audyt, bo widzisz rozkład, a nie średnią, która nie opisuje niczego.
LCP to prawie zawsze obrazek albo serwer
LCP dzieli się na cztery części: czas do pierwszego bajtu, opóźnienie startu ładowania zasobu, czas ładowania zasobu i opóźnienie renderowania. Rozbicie tych czterech składowych od razu mówi, gdzie szukać. Jeśli czas do pierwszego bajtu wynosi 900 ms, optymalizacja obrazu nie ma sensu, dopóki nie ogarniesz warstwy cache po stronie serwera. W praktyce najczęściej wygrywa regeneracja przyrostowa z krótkim czasem rewalidacji oraz nagłówek s-maxage ze stale-while-revalidate na poziomie CDN.
Drugi typowy problem to obraz główny ładowany dopiero po hydratacji, bo siedzi w komponencie klienckim albo w karuzeli. Komponent next/image z atrybutem priority ustawia fetchpriority na high i dokłada wstępne pobranie, ale tylko wtedy, gdy trafia do początkowego HTML. Karuzela renderowana po stronie klienta przesuwa start ładowania o 400-800 ms i żadna optymalizacja formatu tego nie nadrobi.
Fonty potrafią dołożyć 200-300 ms do wyrenderowania tekstu, jeśli ładują się z zewnętrznego hosta. Moduł next/font pobiera je w czasie budowania, serwuje z własnej domeny i generuje metrykę kroju zapasowego, więc przy okazji zdejmuje przesunięcia układu. Jeśli z jakiegoś powodu musisz zostać przy zewnętrznym dostawcy, dodaj preconnect i ustaw display na swap, ale licz się z gorszym wynikiem.
- Rozbij LCP na cztery składowe, zanim cokolwiek zmienisz
- Obraz odpowiedzialny za LCP musi być w początkowym HTML, z priority i poprawnym atrybutem sizes
- Ustaw s-maxage i stale-while-revalidate na CDN dla stron kategorii
- next/font zamiast zewnętrznego hosta fontów
- Sprawdź, czy CDN nie serwuje wariantu bez kompresji dla części klientów
INP to koszt hydratacji i zbyt wielu obcych skryptów
Próg dobrego INP wynosi 200 ms w 75. percentylu i większość sklepów go nie spełnia z jednego powodu: wątek główny jest zajęty w momencie pierwszych interakcji. Klasyczny scenariusz to użytkownik klikający filtr kategorii dwie sekundy po wczytaniu, gdy trwa jeszcze hydratacja listy sześćdziesięciu produktów. Renderowanie serwerowe komponentów z App Routera zdejmuje z tego dużą część, bo komponenty serwerowe nie mają kodu do hydratacji.
Skrypty zewnętrzne są zwykle większym sprawcą niż własny kod. Tag Manager z ośmioma tagami, narzędzie do map ciepła, widżet czatu i skrypt opinii potrafią zająć wątek główny na 1,2 sekundy w blokach po 150-300 ms. Komponent next/script ze strategią lazyOnload dla czatu i opinii oraz przesunięcie mniej istotnych tagów na wyzwalacz oparty na przewinięciu dają zwykle 80-150 ms poprawy INP w danych z pola. Partytown wygląda obiecująco, ale psuje się na skryptach korzystających z document.write i synchronicznego dostępu do ciasteczek, więc traktujemy go jako opcję, nie jako rozwiązanie domyślne.
Wewnątrz samej aplikacji najwięcej daje rozbijanie długich zadań. Jeśli obsługa kliknięcia w filtr wykonuje 40 ms obliczeń, a potem odświeża całą listę, warto oddzielić natychmiastową reakcję wizualną od ciężkiej pracy, oddając wątek przez scheduler.yield albo startTransition. Użytkownik widzi zaznaczony filtr po 30 ms, lista dociera 200 ms później i całość jest odbierana jako szybka.
CLS: fonty, baner zgód i sekcje doładowywane
CLS w sklepach ma zwykle trzy źródła i wszystkie da się naprawić w jeden dzień. Pierwsze to obrazy bez zarezerwowanego miejsca, co rozwiązuje ustawienie proporcji na kontenerze albo podanie wymiarów w next/image. Drugie to baner zgód wstawiany do przepływu dokumentu zamiast na warstwę, przez co cała strona skacze w dół o 90-140 pikseli po mniej więcej 600 ms.
Trzecie źródło jest mniej oczywiste: sekcje doładowywane po hydratacji, na przykład rekomendacje produktowe albo pasek z opiniami. Jeśli komponent renderuje pustkę do czasu odpowiedzi API, a potem 300 pikseli treści, dostajesz przesunięcie wszystkiego, co jest niżej. Szkielet o dokładnie takiej samej wysokości jak docelowa treść kosztuje kwadrans pracy i usuwa problem całkowicie.
Te sytuacje trzeba sprawdzać na wolnym łączu, bo przy szybkim połączeniu lokalnym przesunięcia dzieją się przed pierwszym pomiarem i wyglądają na nieistniejące. W narzędziach deweloperskich ustawiamy dławienie sieci na Fast 4G i czterokrotne spowolnienie procesora, co całkiem dobrze odpowiada telefonom generującym większość ruchu mobilnego w Polsce.
- Baner zgód jako nakładka, nigdy jako element wypychający treść w dół
- Szkielety o wysokości identycznej z docelową treścią, nie paski wysokości 20 pikseli
- Proporcje obrazów ustawione na kontenerze, nie wyliczane po załadowaniu
- Krój zapasowy z dopasowaną metryką, żeby podmiana fontu nie przesuwała tekstu
- Testy przy dławieniu sieci i procesora, inaczej problemu nie zobaczysz
Budżet wydajności w CI, inaczej wszystko wróci
Optymalizacja bez zabezpieczenia w procesie wdrożeniowym utrzymuje się jakieś trzy miesiące. Potem ktoś dodaje bibliotekę do wykresów ważącą 90 kB, ktoś inny wpina kolejny skrypt marketingowy i wracasz do punktu wyjścia. Dlatego każdy projekt kończymy progami w pipelinie: Lighthouse CI z asercjami na LCP i CLS oraz limit rozmiaru pierwszego ładowania JavaScriptu.
Wartości, które ustawiamy domyślnie: pierwsze ładowanie JavaScriptu poniżej 170 kB po kompresji dla stron listingowych, budżet 120 kB w formacie AVIF dla obrazu głównego, brak zadania dłuższego niż 200 ms w symulacji mobilnej. Progi mają blokować pull request, a nie tylko wypisywać ostrzeżenie, bo ostrzeżenia się ignoruje. Raz na kwartał robimy przegląd i podnosimy poprzeczkę, jeśli mamy zapas.
Ile z tego widać w pozycjach i konwersji
Core Web Vitals są częścią sygnałów o jakości strony i wpływ na pozycje jest realny, ale słaby i niesamodzielny. Strona, która nie odpowiada na intencję zapytania, nie wskoczy do pierwszej trójki dlatego, że poprawiła LCP o 400 ms. Traktowanie tych metryk jako dźwigni pozycji kończy się rozczarowaniem i wydawaniem budżetu tam, gdzie nie ma go z czego odzyskać.
Wpływ na konwersję jest znacznie łatwiejszy do obronienia. W dwóch projektach z ostatniego roku poprawa INP z około 380 ms do poniżej 200 ms szła w parze ze spadkiem porzuceń na kroku dostawy o 6-9 procent, mierzonym testem podzielonym na poziomie wdrożenia. Tych liczb nie da się przenieść wprost na inny sklep, ale kierunek jest powtarzalny i wynika z prostej rzeczy: interfejs, który nie odpowiada na dotknięcie, wygląda na zepsuty.
Najważniejsze wnioski
- Decyzje podejmuj na danych z CrUX, Lighthouse służy do diagnozy pojedynczej zmiany.
- Rozbij LCP na cztery składowe, zanim zaczniesz optymalizować obrazy.
- INP w sklepach psują głównie skrypty zewnętrzne i hydratacja dużych list.
- Baner zgód jako nakładka i szkielety o właściwej wysokości usuwają większość CLS.
- Bez progów w CI poprawa wydajności wytrzymuje mniej więcej kwartał.
Częste pytania
Tak, ale jako jeden ze słabszych sygnałów jakości strony. Przy dwóch stronach równie dobrze odpowiadających na zapytanie szybsza może wygrać, natomiast sama poprawa metryk nie przesunie strony, która nie pasuje do intencji. Większą wartość Core Web Vitals mają po stronie konwersji i kosztu pozyskania ruchu płatnego.
Poniżej 200 ms w 75. percentylu danych z pola oznacza ocenę dobrą, przedział 200-500 ms wymaga poprawy, a powyżej 500 ms jest oceniany jako zły. Warto patrzeć na wynik osobno dla urządzeń mobilnych, bo tam próg bywa przekraczany nawet wtedy, gdy średnia dla całej witryny wygląda poprawnie.
Nie. Daje dobre narzędzia: komponent obrazu, ładowanie fontów w czasie budowania, komponenty serwerowe i dzielenie kodu, ale każde z nich trzeba świadomie zastosować. Projekt w Next.js z ciężkim komponentem klienckim na starcie i czterema skryptami marketingowymi wypadnie gorzej niż starannie zrobiona strona w dowolnej innej technologii.