Wynik 95 w PageSpeed Insights nie oznacza, że strona ma dobre Core Web Vitals. To pomiar laboratoryjny z jednego uruchomienia na emulowanym telefonie, a Core Web Vitals to dane od prawdziwych użytkowników zbierane przez 28 dni. Obie liczby są przydatne, ale do czego innego, a obietnica "strona będzie miała świetne Core Web Vitals" złożona na podstawie samego wyniku PageSpeed jest obietnicą, której w dniu odbioru nie da się sprawdzić.
Poniżej opisujemy, jak mierzymy stronę, zanim pokażemy wynik przy odbiorze, i jakich technik używamy, żeby LCP, INP i CLS mieściły się w progach. Wszystkie pochodzą z działającego kodu, w tym z tej strony, na której czytasz ten tekst. Przy każdej piszemy też, co kosztuje, bo żadna z nich nie jest darmowa.
- Wynik PageSpeed to test laboratoryjny. Core Web Vitals to dane terenowe z Chrome. Nowa strona ma tylko ten pierwszy.
- Najwięcej dają: treść w HTML-u zamiast renderowania w przeglądarce, JavaScript uruchamiany po pierwszym malowaniu i obraz LCP pobierany z najwyższym priorytetem.
- CLS psują głównie fonty i elementy bez zarezerwowanego miejsca. INP psują długie zadania na głównym wątku.
- Wynik najczęściej spada po oddaniu strony: przez skrypty zewnętrzne, ciężkie zdjęcia z CMS-a i banery cookies.
Wynik PageSpeed i Core Web Vitals to dwa różne pomiary
PageSpeed Insights pokazuje na jednym ekranie dwie sekcje, które łatwo wziąć za jedno. U góry, jeśli strona ma wystarczający ruch, są dane terenowe z raportu Chrome User Experience Report (CrUX): jak strona zachowywała się u prawdziwych użytkowników Chrome w ostatnich 28 dniach. Niżej jest wynik od 0 do 100, czyli Lighthouse uruchomiony raz na serwerach Google, na emulowanym telefonie ze sztucznie spowolnionym łączem i procesorem.
Liczba od 0 do 100 powstaje z pięciu metryk laboratoryjnych: First Contentful Paint, Speed Index, Largest Contentful Paint, Total Blocking Time i Cumulative Layout Shift. Najmocniej ważą LCP, TBT i CLS. Zauważ, że w tym zestawie nie ma INP, bo w laboratorium nikt nie klika. Jego zastępcą jest Total Blocking Time, który mierzy, jak długo główny wątek był zajęty długimi zadaniami podczas ładowania.
Core Web Vitals oceniane są na 75. percentylu. Strona "przechodzi", gdy co najmniej trzy czwarte odsłon mieści się w progu dla każdej z trzech metryk. Dlatego szybki laptop programisty i światłowód w biurze nic tu nie mówią: liczy się telefon ze średniej półki na zatłoczonym LTE, bo to on wyznacza ten percentyl.
Z tego wynika rzecz, o której trzeba powiedzieć, zanim projekt się zacznie: nowa strona nie ma danych terenowych. CrUX pojawi się dopiero wtedy, gdy strona zbierze odpowiednio dużo odsłon z Chrome, a przy małych stronach firmowych może nie pojawić się wcale. Wynik 95+, który pokazujemy przy odbiorze, jest więc wynikiem laboratoryjnym. Uczciwie jest nazwać go po imieniu, a nie sprzedawać jako "Google ocenia stronę na 95".
Trzy progi i to, co naprawdę mierzą
Google publikuje progi dla każdej metryki. Wartość "dobra" musi być osiągnięta na 75. percentylu odsłon, osobno dla telefonów i komputerów.
| Metryka | Co mierzy | Dobrze | Do poprawy | Źle |
|---|---|---|---|---|
| LCP (Largest Contentful Paint) | Kiedy wyrenderował się największy element w widoku: zdjęcie, blok tekstu, plakat wideo | do 2,5 s | 2,5 do 4 s | powyżej 4 s |
| INP (Interaction to Next Paint) | Ile trwa od kliknięcia, dotknięcia lub klawisza do następnej klatki na ekranie, bierze się najgorsze interakcje z całej wizyty | do 200 ms | 200 do 500 ms | powyżej 500 ms |
| CLS (Cumulative Layout Shift) | Jak bardzo treść przeskakuje bez udziału użytkownika, suma przesunięć w najgorszym oknie czasowym | do 0,1 | 0,1 do 0,25 | powyżej 0,25 |
LCP najczęściej jest zdjęciem w hero albo nagłówkiem H1. Rozkłada się na cztery odcinki: czas do pierwszego bajtu odpowiedzi serwera, opóźnienie, zanim przeglądarka w ogóle zacznie pobierać zasób LCP, czas pobierania tego zasobu i czas renderowania. Gdy LCP jest za wysokie, zanim zaczniesz kompresować zdjęcie, sprawdź, który odcinek jest długi. Bardzo często to drugi: przeglądarka dowiaduje się o obrazie dopiero z JavaScriptu albo z CSS-a, więc zaczyna go pobierać z opóźnieniem.
INP zastąpił w marcu 2024 roku starszą metrykę FID. Różnica jest istotna: FID mierzył tylko opóźnienie przed obsługą pierwszej interakcji, a INP obejmuje wszystkie interakcje w trakcie wizyty i cały czas aż do narysowania odpowiedzi. Filtr w sklepie, który zamraża stronę na 400 ms, był dla FID niewidoczny, jeśli nie był pierwszym kliknięciem. Dla INP jest wynikiem całej strony.
CLS nie liczy przesunięć, które wystąpiły do 500 ms po interakcji użytkownika, więc rozwinięcie akordeonu po kliknięciu nie szkodzi. Szkodzi baner cookies wsuwający się nad treść, obraz bez wymiarów, który po załadowaniu rozpycha tekst, i font, który po podmianie ma inną szerokość liter niż font zastępczy.
Jak mierzymy stronę, zanim pokażemy wynik przy odbiorze
Mierzyć trzeba zbudowaną wersję produkcyjną wystawioną pod adresem testowym, nigdy serwer deweloperski. Serwer deweloperski podaje nieskompresowany i niezminifikowany kod, dokłada moduł przeładowania na żywo i ładuje każdy plik osobno, więc wynik z niego jest zaniżony w sposób, który nic nie mówi o produkcji. Z tego samego powodu środowisko testowe powinno mieć tę samą konfigurację kompresji i nagłówków cache co produkcja, inaczej pomiar opisuje inny serwer niż ten, na którym strona zamieszka.
Każdą ważną podstronę mierzy się osobno, w trybie mobilnym, kilka razy z rzędu. Wynik Lighthouse zmienia się między uruchomieniami o kilka punktów, bo zależy od obciążenia maszyny testującej i sieci. Pojedynczy pomiar 96 i pojedynczy 91 mogą opisywać tę samą stronę. Miarodajna jest mediana serii i na to, czy któraś metryka nie skacze między "dobrze" a "do poprawy".
Strona główna to za mało. Pomiar powinien objąć każdy typ szablonu: stronę główną, podstronę oferty, wpis na blogu, kontakt, a w sklepie kartę produktu i listing. Najcięższa bywa podstrona, na którą nikt nie patrzy przy odbiorze, na przykład galeria realizacji z trzydziestoma zdjęciami albo strona z osadzoną mapą.
Wyniki pokazujemy klientowi przed odbiorem. Lepiej niż zrzut ekranu z jedną liczbą działa link do raportu: PageSpeed Insights udostępnia wynik pod stałym adresem, a na stronie raportu każdy może uruchomić test ponownie, więc klient nie musi wierzyć wykonawcy na słowo. Standard 95+ w PageSpeed dotyczy stron. Jak podchodzimy do paneli za logowaniem, gdzie PageSpeed w ogóle nie dociera, piszemy w ostatniej sekcji.
Lighthouse w Chrome DevTools uruchamiany na własnym komputerze daje inne wyniki niż PageSpeed Insights, bo symuluje spowolnienie na Twoim sprzęcie, a do tego liczą się rozszerzenia przeglądarki. Do porównań z klientem używaj PageSpeed Insights albo Lighthouse w oknie incognito bez rozszerzeń.
Treść w HTML-u: prerendering każdej podstrony
Największa pojedyncza zmiana dla LCP na stronie zbudowanej w React to wysyłanie gotowego HTML-u z treścią. Aplikacja renderowana wyłącznie w przeglądarce wysyła pusty kontener, a nagłówek i tekst pojawiają się dopiero, gdy przeglądarka pobierze, sparsuje i wykona cały JavaScript. Na emulowanym telefonie w PageSpeed ten łańcuch potrafi zająć kilka sekund, zanim cokolwiek się narysuje.
Dlatego każda podstrona jest u nas renderowana do statycznego HTML-u w czasie budowania. Skrypt przechodzi po liście tras we wszystkich językach, renderuje każdą z nich po stronie serwera i zapisuje wynik jako osobny plik. Przeglądarka dostaje od razu nagłówek, akapity i obrazy, a przy okazji robot wyszukiwarki widzi pełną treść bez wykonywania JavaScriptu. Ten sam mechanizm generuje mapę strony, więc lista adresów w sitemap zawsze zgadza się z tym, co naprawdę istnieje.
Do HTML-u dołączamy zserializowany stan danych, z których strona została wyrenderowana. Gdy aplikacja uruchamia się w przeglądarce, czyta ten stan zamiast pobierać te same dane drugi raz. Bez tego po starcie JavaScriptu strona na moment pokazywałaby stan ładowania, a potem ponownie wstawiała treść, co psuje i CLS, i wrażenie szybkości.
Jest jedna pułapka, o którą łatwo się potknąć. Podstrony ładowane leniwie przez React.lazy przy renderowaniu przez renderToString nie czekają na załadowanie swojego kodu. Pierwsza trasa, która dotknie takiego komponentu, dostaje w HTML-u pusty znacznik "przełącz na renderowanie w przeglądarce" i cała korzyść z prerenderingu znika po cichu, bez błędu w konsoli. Rozwiązaliśmy to przebiegiem rozgrzewającym: najpierw renderujemy wszystkie trasy i wynik wyrzucamy, żeby każdy leniwy moduł zdążył się załadować, a dopiero drugi przebieg zapisuje pliki.
Kosztem prerenderingu jest czas budowania i to, że zmiana treści wymaga ponownego wygenerowania stron. Na blogu rozwiązaliśmy to tak, że wpisy są plikami Markdown czytanymi przez generator bezpośrednio z dysku. Nowy wpis nie wymaga więc przebudowy całej aplikacji, tylko ponownego uruchomienia samego kroku generowania HTML-u.
JavaScript startuje dopiero po pierwszym malowaniu
Skoro treść jest w HTML-u, JavaScript nie musi blokować pierwszego wyświetlenia. Standardowo przeglądarka i tak zaczyna pobierać moduły wskazane w HTML-u równolegle z renderowaniem, a ich parsowanie i wykonanie zajmuje główny wątek dokładnie wtedy, gdy powinien rysować stronę. To podbija Total Blocking Time w PageSpeed i opóźnia LCP.
Usuwamy więc z wygenerowanego HTML-u znaczniki skryptów modułowych i podpowiedzi modulepreload, a w ich miejsce wstawiamy krótki skrypt, który dodaje je z powrotem dopiero po zdarzeniu First Contentful Paint. Zasada wygląda tak (ścieżka do pliku jest przykładowa, w buildzie nazwa zawiera hash):
Dwa zabezpieczenia na dole nie są ozdobą. Karta otwarta w tle nie maluje niczego, więc zdarzenie FCP nie przyjdzie, dopóki użytkownik jej nie pokaże; wtedy uruchamiamy skrypty od razu po zdarzeniu load. Drugie zabezpieczenie, uruchomienie po trzech sekundach od load, chroni przed przeglądarką, w której obserwator zdarzeń malowania z jakiegoś powodu nie zadziałał. Blok try obsługuje przeglądarki bez PerformanceObserver.
Kompromis trzeba znać, zanim się go wdroży. Przez ułamek sekundy po pierwszym wyświetleniu strona jest czytelna, ale nie jest jeszcze interaktywna: przycisk otwierający formularz nie zareaguje, dopóki aplikacja się nie uruchomi. Dlatego linki są zwykłymi znacznikami <a> z prawdziwym adresem, a nie elementami działającymi tylko przez JavaScript. Kliknięcie w link przed startem aplikacji po prostu przeniesie na podstronę.
Obok tego każda trasa dostaje w nagłówku modulepreload tylko dla fragmentów kodu, których faktycznie potrzebuje: wspólnego rdzenia, słownika języka i modułu danej podstrony. Strona kontaktu nie pobiera kodu bloga, a wpis na blogu nie pobiera formularza konfiguratora.
Obraz LCP: kolejność pobierania ważniejsza niż rozmiar pliku
Gdy elementem LCP jest obraz, liczy się moment, w którym przeglądarka zaczyna go pobierać. Obraz ustawiony jako tło w CSS albo wstawiany przez komponent po starcie aplikacji zostanie odkryty późno, nawet jeśli waży kilkadziesiąt kilobajtów. Dlatego generator HTML-u po wyrenderowaniu każdej podstrony szuka pierwszego obrazu z treści, pomijając logo, flagi i elementy nawigacji, i dopisuje do nagłówka podpowiedź pobrania z wysokim priorytetem.
Atrybuty width i height nie ustalają rozmiaru wyświetlania, bo ten nadal kontroluje CSS. Pozwalają przeglądarce policzyć proporcje i zarezerwować miejsce, zanim obraz się pobierze, co chroni CLS. Jeśli obraz ma kilka wariantów rozmiaru przez srcset, podpowiedź preload musi mieć odpowiadające atrybuty imagesrcset i imagesizes, inaczej przeglądarka pobierze jeden plik z preloadu i drugi z srcset. Nasz generator z tego powodu pomija obrazy z srcset przy automatycznym preloadzie.
Łatwy do przeoczenia błąd to loading="lazy" ustawione globalnie na wszystkich obrazach, łącznie z tym w hero. Leniwe ładowanie czeka, aż przeglądarka ustali układ strony i sprawdzi, czy obraz jest blisko widoku, więc obraz LCP startuje z opóźnieniem. loading="lazy" stosujemy wyłącznie poniżej pierwszego ekranu.
Formatem domyślnym jest WebP, a dla małych kart generujemy osobne warianty o szerokości 320 pikseli zamiast skalować w przeglądarce plik przygotowany pod pełną szerokość ekranu. Karta usługi o szerokości kilkuset pikseli nie potrzebuje pliku, który na monitorze 4K wyglądałby dobrze na całą szerokość.
Fonty, które nie przesuwają układu
Font z sieci wpływa na dwie metryki naraz. Jeśli przeglądarka czeka na font, tekst jest niewidoczny i opóźnia LCP. Jeśli rysuje tekst fontem systemowym i potem podmienia go na docelowy, litery zmieniają szerokość, wiersze łamią się inaczej i cały blok przesuwa się w dół, co liczy się do CLS.
Pierwszy krok to fonty hostowane na tej samej domenie w formacie WOFF2, podzielone na podzbiory znaków przez unicode-range. Przeglądarka pobiera plik tylko wtedy, gdy na stronie występuje znak z jego zakresu. Tu jest szczegół ważny dla polskich stron: litery ą, ć, ę, ł, ń, ś, ź i ż leżą w zakresie Latin Extended-A, a ó w podstawowym Latin-1. Polska strona pobiera więc zawsze dwa pliki, podstawowy i rozszerzony, a strona rosyjska dodatkowo plik z cyrylicą. W nagłówku preloadujemy tylko podstawowy podzbiór, bo tylko on jest potrzebny na każdej podstronie w każdym języku.
Drugi krok to font zastępczy dopasowany metrykami do docelowego. CSS pozwala zbudować własną definicję fontu na bazie fontu systemowego i przeskalować jego wysokość, wydłużenia górne i dolne tak, żeby tekst zajmował tyle samo miejsca co font docelowy. Wtedy font-display: swap pokazuje tekst od razu, a podmiana nie przesuwa wierszy.
Wartości size-adjust i nadpisań nie zgaduje się. Dobiera się je, renderując ten sam akapit fontem docelowym i zastępczym jeden na drugim i poprawiając liczby, dopóki wiersze nie łamią się w tych samych miejscach. Dla każdej pary fontów wartości są inne, więc nie kopiuj ich z cudzego projektu.
Na koniec, mniej wariantów to mniej plików. Jeśli projekt używa grubości 300, 500 i 700 jednego kroju, a różnica między 500 i 700 w nagłówkach jest ledwo widoczna, rezygnacja z jednej grubości to jeden plik mniej na każdej stronie.
Animacje wejścia w CSS zamiast w JavaScripcie
Nasze strony mają ruch: nagłówek wjeżdża od dołu, przyciski pojawiają się z opóźnieniem, karty wchodzą po kolei. Gdyby tę animację sterowała biblioteka w JavaScripcie, element pozostałby w stanie początkowym do chwili, aż biblioteka się pobierze i uruchomi. Przy skryptach startujących po pierwszym malowaniu oznaczałoby to, że hero czeka na JavaScript, czyli dokładnie to, czego chcieliśmy uniknąć.
Dlatego animacje wejścia w hero to zwykłe @keyframes w CSS, które przeglądarka uruchamia razem z pierwszym renderem. Nagłówek i opis animują wyłącznie transform, bez opacity. Element przesunięty o 24 piksele jest już widoczny i przeglądarka może go policzyć jako wyrenderowaną treść, natomiast tekst startujący z pełnej przezroczystości nie jest dla użytkownika widoczny, dopóki się nie pojawi, co przesuwa LCP na koniec animacji. Zanikanie z przezroczystości zostawiamy dla przycisków, które elementem LCP nie są.
Klasa .js na elemencie <html> jest dodawana przez jednolinijkowy skrypt w nagłówku, jeszcze przed wczytaniem arkuszy. Jeśli JavaScript jest wyłączony, klasy nie ma, animacja się nie włącza i treść stoi na swoim miejscu od początku. Użytkownik z włączonym ograniczeniem ruchu w systemie dostaje stronę bez animacji wejścia.
Dla elementów, które mają zachowywać się jak sprężyna, a nie jak zwykłe wygaszenie, przeliczamy krzywą sprężyny raz, na etapie pisania stylów, do funkcji linear() w CSS z kilkudziesięcioma punktami. Przeglądarka odtwarza ją jak każdą inną krzywą czasu, bez biblioteki fizyki działającej w każdej klatce. Animacja przesunięcia i skali działa na warstwie kompozytora, więc nie wywołuje ponownego przeliczania układu i nie przesuwa sąsiednich elementów, czyli nie liczy się do CLS.
Druga pułapka dotyczy tekstu, który się zmienia, na przykład rotującego hasła w nagłówku. Jeśli kolejne warianty mają różną długość, nagłówek raz zajmuje jeden wiersz, raz dwa, i wszystko pod nim skacze. Rozwiązaniem jest nagłówek o stałej wysokości, liczonej w jednostkach em na najdłuższy wariant, z tekstem pozycjonowanym wewnątrz.
Mniej bajtów na łączu: kompresja z góry i podział kodu
Serwer może kompresować pliki w locie przy każdym żądaniu, ale wtedy używa niższego poziomu kompresji, bo najwyższy jest zbyt kosztowny dla procesora przy każdym zapytaniu. Pliki statyczne kompresujemy więc raz, w czasie budowania: Brotli z najwyższą jakością i gzip z najwyższym poziomem dla HTML-u, JavaScriptu, CSS-a, JSON-a, SVG i XML-a. Pomijamy pliki mniejsze niż kilobajt, bo zysk jest pomijalny, a nagłówki odpowiedzi i tak ważą swoje. Serwer podaje gotowy plik .br przeglądarce, która go obsługuje, a .gz pozostałym.
Dyrektywa gzip_static jest w standardowym nginx, natomiast brotli_static wymaga modułu Brotli. Nagłówek immutable z rocznym czasem ważności jest bezpieczny tylko dlatego, że nazwy plików w katalogu zasobów zawierają hash treści. Każda zmiana kodu daje nową nazwę, więc przeglądarka nigdy nie użyje starej wersji z pamięci podręcznej. HTML dostaje krótki czas ważności, bo to on wskazuje aktualne nazwy plików.
Kod aplikacji dzielimy na osobne paczki według tego, jak często się zmienia i gdzie jest używany. React i router trafiają do wspólnej paczki dostawców, biblioteka animacji do osobnej, a parser Markdown z podświetlaniem składni do jeszcze innej, pobieranej tylko na blogu. Poprawka w komponencie strony głównej nie unieważnia w pamięci podręcznej użytkownika kodu Reacta, który nie zmienił się od miesięcy.
W bibliotece do podświetlania składni rejestrujemy ręcznie tylko te języki, o których faktycznie piszemy, zamiast importować pakiet domyślny ze wszystkimi. Pełny import jest wygodny, ale ciągnie definicje wszystkich obsługiwanych języków, z których żaden wpis nie użyje większości. Takie decyzje nie są widoczne w pojedynczym pomiarze, ale sumują się na każdej podstronie.
Ostatni element to service worker. Pierwotnie serwował pliki najpierw z własnej pamięci podręcznej, co przy każdym wdrożeniu kończyło się sytuacją "nie widzę zmian". Obecnie działa w trybie najpierw sieć: zawsze pobiera świeżą wersję, a pamięć podręczną traktuje wyłącznie jako zapas na brak połączenia. Nie ma to kosztu wydajnościowego, bo pliki z hashem w nazwie i tak przychodzą natychmiast z pamięci podręcznej HTTP przeglądarki.
INP: kliknięcie ma dostać odpowiedź w następnej klatce
INP nie da się sprawdzić w PageSpeed, bo laboratorium nie klika. Na stronie firmowej zwykle nie jest problemem, dopóki nie pojawi się coś, co przetwarza dane w przeglądarce: filtr listy realizacji, wyszukiwarka, kalkulator wyceny, formularz z walidacją na każdy znak. Wtedy jedno kliknięcie uruchamia pętlę po kilku tysiącach elementów, a przeglądarka nie może narysować żadnej klatki, dopóki pętla się nie skończy.
Pierwsza zasada: najpierw pokaż reakcję, potem licz. Zmiana stanu przycisku, wyróżnienie wybranego filtra, wskaźnik ładowania idą do pierwszej klatki, a ciężkie obliczenia dzielimy na kawałki i oddajemy przeglądarce kontrolę między nimi, żeby mogła narysować odpowiedź.
scheduler.yield() jest dostępne w Chrome i przeglądarkach na tym samym silniku. Tam, gdzie go nie ma, setTimeout z zerowym opóźnieniem daje ten sam efekt z nieco gorszym priorytetem wznowienia. Co ile elementów oddawać kontrolę, zależy od kosztu jednego sprawdzenia; celem jest, żeby żaden kawałek nie trwał dłużej niż kilkadziesiąt milisekund.
Druga zasada dotyczy tego, co dzieje się w stylach po kliknięciu. Rozwijanie panelu przez animowanie height wymusza przeliczenie układu w każdej klatce, a przy dłuższej stronie to realny koszt. Animujemy transform i opacity, a do rozwijania treści używamy technik, które nie przeliczają układu sąsiadów w każdej klatce. W kodzie obsługi zdarzeń unikamy przeplatania odczytów wymiarów, takich jak offsetHeight, z zapisami stylów, bo każdy taki odczyt po zapisie wymusza synchroniczne przeliczenie układu.
Trzecia zasada: mierz na słabym telefonie. W zakładce Performance w DevTools można spowolnić procesor kilkukrotnie i nagrać kliknięcie. Każde zadanie dłuższe niż 50 ms jest zaznaczone na czerwono, a panel pokazuje, która funkcja je zajęła. Na komputerze deweloperskim ten sam filtr może wyglądać na natychmiastowy.
Co psuje wynik po oddaniu strony
Strona z zielonym wynikiem przy odbiorze potrafi po kilku miesiącach spaść do pomarańczowego, choć nikt nie zmienił w niej linii kodu. Powody są powtarzalne i prawie zawsze pochodzą spoza kodu, który oddaliśmy.
- Skrypty zewnętrzne dodane przez menedżer tagów. Piksel reklamowy, mapa ciepła, widżet czatu i narzędzie do testów A/B ładują własny JavaScript na głównym wątku. Narzędzia do testów A/B potrafią dodatkowo ukrywać całą stronę, dopóki nie zdecydują o wariancie, co bezpośrednio przesuwa LCP.
- Zdjęcia wgrywane prosto z aparatu. Panel treści, który nie skaluje i nie konwertuje obrazów przy wgrywaniu, pozwala wstawić do hero plik o rozmiarze kilku megabajtów.
- Baner zgody na cookies. Baner, który po załadowaniu wsuwa się na górę strony i spycha treść w dół, generuje przesunięcie układu przy każdej pierwszej wizycie.
- Osadzone wideo i mapy. Pojedynczy iframe odtwarzacza wideo pobiera setki kilobajtów skryptów, zanim ktokolwiek naciśnie przycisk odtwarzania.
- Nowe fonty i sekcje dodane bez pomiaru. Każda zmiana z osobna wygląda niewinnie, razem odwracają pracę z etapu budowy.
Na każdy z tych przypadków jest wzorzec, który nie wymaga rezygnacji z narzędzia. Skrypty marketingowe można ładować po zgodzie i po zdarzeniu load, a nie w nagłówku. Wideo osadza się jako fasadę: statyczny obraz z przyciskiem, który podmienia się na odtwarzacz dopiero po kliknięciu. Baner cookies powinien nakładać się na treść w stałej pozycji na dole ekranu, zamiast ją przesuwać. Panel treści powinien skalować obraz przy wgrywaniu, zanim ktokolwiek go opublikuje.
Po wdrożeniu nasze projekty są objęte bezpłatnym monitoringiem, najczęściej przez 90 dni. To dokładnie okres, w którym strona dostaje pierwszy prawdziwy ruch, a klient zaczyna dodawać własne treści i narzędzia, więc zakres tego, co w nim obserwujemy, warto ustalić wprost przy odbiorze. Jeśli strona ma za mało ruchu, żeby Google pokazało dla niej dane terenowe, można zbierać je samodzielnie biblioteką web-vitals, wysyłając wyniki z przeglądarek odwiedzających do własnego punktu zbierania:
navigator.sendBeacon wysyła dane także wtedy, gdy użytkownik zamyka kartę, a to jest moment, w którym biblioteka raportuje ostateczne wartości INP i CLS. Pole rating przyjmuje wartości good, needs-improvement albo poor według tych samych progów, które publikuje Google. Do tego zbierania potrzebna jest zgoda zgodna z polityką prywatności strony, jeśli dane są łączone z identyfikatorem użytkownika.
Kiedy 95 w PageSpeed nie jest właściwym celem
Wynik 95+ jest naszym standardem dla stron: landingów, stron firmowych, blogów, podstron ofertowych. Nie przenosimy go automatycznie na każdy typ projektu, bo w części z nich ta liczba mierzy coś, co nie ma znaczenia.
Panel administracyjny albo system za logowaniem jest dla PageSpeed niedostępny, bo narzędzie nie przejdzie ekranu logowania. Nawet gdyby przeszło, pierwsze wczytanie panelu zdarza się raz dziennie, a potem użytkownik pracuje w nim godzinami. Tam mierzymy to, co odczuwa operator: czas odpowiedzi po kliknięciu, czas zapytań do API, zachowanie tabel z tysiącami wierszy. INP jest w panelu ważniejsze niż LCP.
W sklepie internetowym wynik zależy od decyzji biznesowych, na które nie mamy wyłącznego wpływu: bramki płatności, systemu opinii, integracji z porównywarkami i narzędzi reklamowych. Budujemy kod sklepu tak, żeby sam z siebie mieścił się w progach, a przy każdym dodatkowym skrypcie mówimy, ile kosztuje w pomiarze, zanim zostanie dodany. Decyzja, czy dany piksel reklamowy jest wart kilku punktów, należy do klienta, ale powinien ją podejmować, znając cenę.
Jeśli planujesz stronę i chcesz zobaczyć, jak ten proces wygląda od briefu do odbioru, opisaliśmy go na stronie jak pracujemy. Zakres i ceny startowe znajdziesz przy stronach firmowych, od 1 199 zł, i przy landingach, od 499 zł. Jeśli masz już stronę i chcesz wiedzieć, dlaczego jej wynik spadł, napisz do nas z adresem, a odpowiemy w godzinę.



