Strona może otwierać się szybko, a mimo to reagować z opóźnieniem na kliknięcia. Może też wyświetlać treść późno albo przesuwać elementy podczas ładowania. Dlatego przed optymalizacją sprawdzam, czy problem dotyczy LCP, INP, CLS, czy jeszcze innego fragmentu działania witryny. Siedem opisanych dalej napraw obejmuje obrazy, serwer, JavaScript, CSS, fonty i ładowanie zasobów. Przy każdej podaję, kiedy warto ją rozważyć i jak zmierzyć rezultat.
Zanim zaczniesz: zmierz Core Web Vitals na właściwej stronie
Core Web Vitals to trzy metryki Google: szybkość pojawienia się głównej treści, reakcja na interakcję i stabilność układu. Według strony Google o page experience są używane w systemach rankingowych, ale dobre wyniki nie gwarantują pozycji. Wydajność poprawiam więc dla ludzi i nie obiecuję po niej konkretnego wzrostu ruchu ani sprzedaży.
Zacznij od reprezentatywnych adresów, po jednym z każdego ważnego typu strony:
- strona wejściowa,
- kategoria,
- produkt,
- artykuł,
- ważny formularz albo koszyk.
Pierwszą wizytę i powrót badaj osobno, tak jak anonimowy test strony głównej i zakup zalogowanego klienta.
Core Web Vitals: trzy różne cele
W danych terenowych porównuj 75. percentyl, osobno dla telefonu i komputera, bo średnia i najlepszy przebieg wprowadzą cię w błąd. Docelowo wszystkie trzy metryki mają mieścić się w dobrym zakresie (web.dev).
TTFB, czyli czas do pierwszego bajtu odpowiedzi, pomaga szukać przyczyn i stoi poza trzema metrykami Core Web Vitals.
Core Web Vitals Triage
Wpisz wartości LCP, INP, CLS i TTFB z PageSpeed Insights, Lighthouse albo CrUX, aby ustalić pierwsze wąskie gardło. Do pomiaru URL-a użyj audytora wydajności.
Brak INP to brak danych. Według dokumentacji PageSpeed Insights narzędzie potrafi dać pozytywną ocenę CWV na samych dobrych LCP i CLS, gdy danych INP jest za mało, a o obsłudze kliknięć taka etykieta nic nie mówi.
Lighthouse, CrUX i własne pomiary użytkowników
| Źródło | Do czego go użyć | Czego z niego nie wywnioskować |
|---|---|---|
| Lighthouse / część laboratoryjna PageSpeed | Odtworzenie ładowania i znalezienie problemów zasobów | Zwykły test nawigacyjny bez interakcji nie mierzy INP; TBT nie jest jego zamiennikiem liczbowym |
| CrUX / część terenowa PageSpeed | Ocena doświadczeń kwalifikujących się użytkowników Chrome | Wynik originu nie jest wynikiem konkretnej podstrony; brak danych nie oznacza zera |
| Własny RUM (pomiary rzeczywistych użytkowników) | Analiza wersji wdrożeń, typów stron i interakcji | Dane wymagają poprawnej implementacji; nie muszą odpowiadać populacji i metodzie CrUX |
Prawdziwe wizyty odbywają się na innych urządzeniach i łączach, więc wynik terenowy może wyglądać inaczej (web.dev). Do własnego pomiaru nie wysyłaj treści formularzy, identyfikatorów klientów ani parametrów URL z danymi osobowymi.
CrUX w PageSpeed i w bieżącym API liczy ruchome okno 28 dni i aktualizuje się codziennie. Po wdrożeniu nowe wizyty stopniowo zastępują stare (dokumentacja CrUX API), a po 28 dniach okno obejmuje już tylko nową wersję. Miesięczny zbiór w BigQuery to osobny produkt danych, więc zapisuj daty okresu zbierania.
Brak danych oznacza za mało kwalifikujących się wizyt albo niespełnione warunki włączenia do zbioru. Metodologia CrUX pomija Safari i Chrome na iOS, więc test na prawdziwym iPhonie ma sens także przy dobrym mobilnym wyniku w CrUX.
Krok 1: Przyspiesz obraz LCP i pozwól przeglądarce wcześnie go odkryć
Najpierw znajdź w raporcie element LCP i sprawdź go osobno na telefonie i na komputerze, bo na każdym może to być inny element. Według web.dev kandydatem może być każdy blok tekstu, także nagłówek.
Dla obrazu rozbij czas oczekiwania na cztery odcinki:
Gdy obraz dalej czeka na JavaScript albo arkusz CSS, zysk z mniejszego pliku może w ogóle nie pojawić się w końcowym LCP (web.dev).
Adres obrazu udostępnij w początkowym HTML przez src albo srcset, bo adres schowany wyłącznie w data-src, który odczyta dopiero skrypt, przeglądarka odkryje za późno.
Strona testowa była syntetyczna i bez sieci, więc różnicę robi głównie narzucony timer, ale na prawdziwej stronie mechanizm jest ten sam.
Obrazu LCP nie ładuj leniwie (loading="lazy"), bo przeglądarka odłoży jego pobieranie. Ważnemu zasobowi pierwszego ekranu fetchpriority="high" pomaga dostać odpowiedni priorytet, ale to tylko wskazówka 8. Stosuj go oszczędnie, bo przy masowym użyciu zasoby zaczną ze sobą konkurować.
Dobierz wymiary, format i jakość obrazu
Porównaj obecny plik z WebP i AVIF przy jakości dobranej do grafiki i obejrzyj drobny tekst oraz krawędzie. Przy gradientach i przezroczystości PNG albo SVG bywa lepszy, a web.dev przestrzega przed zakładaniem stałego procentu oszczędności.
Telefon potrzebuje czasem obrazu o większej liczbie pikseli niż jego szerokość w CSS, bo liczy się miejsce w układzie i gęstość ekranu. srcset podaje kandydatów, a sizes opisuje szerokość wyświetlania. Przy błędnych wartościach same atrybuty nic nie dadzą (MDN).
Przykład dla obrazu o maksymalnej szerokości 1120 px i marginesach po 20 px na mniejszym ekranie (nazwy plików są przykładowe):
<img
class="hero-image"
src="/images/produkt-1200.webp"
srcset="/images/produkt-640.webp 640w,
/images/produkt-1200.webp 1200w,
/images/produkt-1920.webp 1920w"
sizes="(min-width: 1160px) 1120px, calc(100vw - 40px)"
width="1200"
height="800"
loading="eager"
fetchpriority="high"
alt="Widok produktu z otwartą komorą główną"
>
.hero-image {
display: block;
width: calc(100% - 40px);
max-width: 1120px;
height: auto;
margin-inline: auto;
}
Ten CSS zakłada rodzica na pełną szerokość, bez paddingu, więc w prawdziwym komponencie dopasuj sizes do układu, nigdy odwrotnie.
Sprawdź efekt w panelu Network i w zapisie Performance. Przeglądarka wybiera właściwy plik, żądanie startuje wcześniej albo transfer jest mniejszy, a LCP w porównywalnych testach się nie pogarsza. Preload dodaj dopiero wtedy, gdy potwierdzisz, że obraz jest odkrywany późno, bo zbędne wskazówki konkurują z innymi zasobami.
Krok 2: Skróć odpowiedź serwera (TTFB) i dobierz bezpieczny cache
Opóźnienie sprzed otrzymania HTML skracasz na serwerze, bo kompresja zdjęć go nie ruszy.
Web.dev wymienia przyczyny wysokiego TTFB: przekierowania, zestawianie połączenia, odległość, chybienie w cache i pracę aplikacji, a droższy hosting leczy tylko część z nich.
Sprawdź czas generowania odpowiedzi, zapytania do bazy i zależności od zewnętrznych API. Treści niezmienne między wizytami prerenderuj albo trzymaj w cache odpowiedzi. Trafienia i chybienia zapisuj osobno, bo różnica między nimi mówi więcej niż logo CDN w panelu hostingu.
Cache przeglądarki i CDN wykonują różne zadania
Cache przeglądarki pozwala temu samemu użytkownikowi ponownie użyć pobranych zasobów, a cache współdzielony, np. CDN, odpowiada także komuś, kto wchodzi pierwszy raz. Efekt zależy od tego, co faktycznie przechowuje (MDN), więc to sprawdź.
| Rodzaj odpowiedzi | Przykład Cache-Control | Warunek |
|---|---|---|
| Publiczny CSS lub JS z hashem zawartości w nazwie | public, max-age=31536000, immutable | Każda zmiana tworzy nowy URL; istniejący plik nie zmienia treści |
| Publiczny HTML wymagający sprawdzania aktualności | no-cache | Odpowiedź może być zapisana, ale przed ponownym użyciem wymaga walidacji |
| Wrażliwa odpowiedź konta lub płatności | private, no-store | Przykład ostrożnej polityki; reguły CDN i aplikacji również muszą ją respektować |
no-store zabrania przechowywania odpowiedzi w cache, do którego dyrektywa ma zastosowanie (MDN). Koszyk i konto klienta trzymaj poza publicznym cache, bo samo cookie w odpowiedzi nie chroni jej przed cache współdzielonym.
Sprawdź efekt na pierwszej wizycie, powrocie, aktualizacji wersji oraz przy logowaniu i wylogowaniu. Nagłówki i trafienia w cache są właściwe, nowe pliki pojawiają się po wdrożeniu, a nikt nie dostaje cudzej treści. Rocznego cache nie odwołasz później samą zmianą nagłówka na serwerze.
Krok 3: Wytnij zbędny JavaScript i CSS, potem sprawdź kompresję
Najwięcej zyskujesz na kodzie pobieranym bez potrzeby, np. gdy strona artykułu ładuje edytor, bibliotekę wykresów albo obsługę formularza używaną gdzie indziej. Podział paczek według podstron i funkcji ogranicza pracę na starcie.
Przyznam się do wpadki na stronie głównej cometweb.io: najpierw odroczyłem arkusze CSS i zyskałem około 0,1 s na pierwszym wyświetleniu, a LCP stał w miejscu.
Hamulcem był JavaScript, więc wyciąłem z kodu przed pierwszym renderem słownik i tabelę meta. JS przed pierwszym renderem spadł wtedy z 329 do 208 KB (gzip), a LCP w laboratoryjnym teście Lighthouse (symulowany profil mobilny) z 6,39 do 5,46 s. Całą diagnozę opisałem w porównaniu danych laboratoryjnych i CrUX.
Panel Coverage w DevTools pokazuje nieużywany kod tylko w nagranym scenariuszu (dokumentacja). Zanim usuniesz reguły, otwórz menu, wywołaj błąd formularza i sprawdź wersję mobilną.
Mały krytyczny CSS możesz wstawić w HTML, ale całego arkusza tam nie przenoś, bo powiększysz dokument i stracisz osobne cache'owanie. Stylów pierwszego ekranu nie odkładaj, bo użytkownik zobaczy nieostylowaną treść albo przesunięcie układu 7.
defer przesuwa wykonanie skryptu, koszt zostaje
Dla zewnętrznego klasycznego skryptu zależnego od dokumentu przydaje się:
<script src="/assets/menu.abc123.js" defer></script>
defer pozwala pobierać skrypt podczas parsowania HTML i wykonać go później, w kolejności skryptów odroczonych. async wykonuje skrypt zaraz po pobraniu, bez tej gwarancji. Skrypty type="module" są odroczone domyślnie (MDN).
Po zmianie sprawdź koszt parsowania i wykonania, bo skrypt może przestać blokować parser, a potem zablokować główny wątek, gdy użytkownik kliknie menu.
Nazwy z raportu: render-blocking resources, excessive DOM, enormous network payload
- Render-blocking resources to arkusze CSS i skrypty, które wstrzymują renderowanie głównej treści. Naprawiasz je krokami z tej sekcji.
- Excessive DOM to zbyt wiele elementów HTML. Duże drzewo podnosi koszt każdej zmiany układu i może spowalniać reakcję na kliknięcia (web.dev), więc upraszczaj zagnieżdżenia, usuwaj zbędne kopie komponentów i ogranicz listy renderowane od razu.
- Enormous network payload to za dużo danych przy pierwszym widoku. Pomaga kompresja obrazów, mniej fontów i bibliotek oraz treść spod pierwszego ekranu na żądanie (web.dev).
Dwa ostatnie łatwo pomylić, bo strona z małym transferem może mieć ogromny DOM generowany po stronie klienta, a wtedy kompresja niczego nie zmieni.
Minifikacja i kompresja to osobne kroki
Minifikacja skraca zapis kodu, a kompresja HTTP, np. Brotli albo gzip, zmniejsza przesyłaną odpowiedź tekstową. Niepotrzebną funkcję i tak trzeba usunąć, a obietnicy „20% szybszej strony” do żadnej z nich nie dopisuj 9.