Szybka odpowiedź: co wykazał audyt techniczny SEO i wydajności Inspektomat.pl?
- Problemem była wydajność na telefonie, ponieważ kategorie SEO, Ułatwienia dostępu i Sprawdzone metody miały po 100 punktów na obu urządzeniach.
- Największy koszt generował prefetch tras, których użytkownik nie wybrał: 45 żądań i ok. 444 KiB, czyli prawie jedna trzecia transferu.
- Główny obraz (hero), czyli element LCP, pobierał się w pełnym rozmiarze, bo przy
images.unoptimized: truekomponentnext/imagenie generujesrcset. - Mapa ładowana bez przewinięcia dokładała ok. 291 KiB kafelków przy wejściu, ale HAR wykazał, że nie opóźniała pobrania głównego obrazu, więc ostatecznie dostała średni priorytet.
- Po dwóch etapach wdrożeń transfer strony głównej spadł o 65%, a wynik mobilny wzrósł z 66 do 94 punktów. Pozostały LCP 2,85 s ogranicza przepustowość: obraz dzieli łącze z prawie 400 KiB JavaScriptu, fontów i CSS.
Inspektomat.pl: serwis w Next.js i zakres audytu
Inspektomat.pl to ogólnopolski katalog firm wykonujących przeglądy instalacji i budynków, w którym użytkownik wybiera miasto i usługę, przechodzi do ofert lub zapoznaje się z rozbudowanym poradnikiem. Strona główna witryny łączy więc treść informacyjną z elementami interaktywnymi, czyli selektorem usług, kartami, mapą oraz FAQ. Sam projekt został oparty na Next.js i jest eksportowany do statycznego HTML. Treść jest w związku z tym gotowa przed wizytą użytkownika i robota, więc jest to dobra podstawa pod , ale to oczywiście nie przesądza o tym, ile pracy przeglądarka wykona po wejściu na stronę: obrazy, style i skrypty nadal trzeba pobrać, a komponenty uruchomić.

Strona główna Inspektomat.pl na komputerze. Główny obraz z wyszukiwarką miasta to element LCP, a karty rozwiązań prowadzą do podstron, które wcześniej pobierały się z wyprzedzeniem.
Ogólny zakres takiego audytu opisuję w artykule co obejmuje audyt techniczny SEO. W tym przypadku audyt opierał się na pięciu źródłach:
- PageSpeed Insights z 23 i 26 października 2026 r. — wynik produkcyjny przed zmianami i po każdym z dwóch etapów wdrożeń,
- — dane rzeczywistych użytkowników (w tym przypadku na poziomie całej domeny),
- nagrania HAR przed zmianami i po nich — pełna lista żądań z czasami i inicjatorami,
- przegląd kodu — żeby wyjaśnić, dlaczego zasoby pobierają się tak, jak pokazuje HAR,
- lokalne pomiary Lighthouse przed zmianami i po nich, plus testy funkcjonalne.
Audyt SEO: indeksacja bez zastrzeżeń, problemem wydajność mobilna
Kategorie SEO, Ułatwienia dostępu i Sprawdzone metody w PageSpeed Insights wykazały po 100 punktów zarówno na komputerach, jak i na telefonach, czyli automatyczny audyt nie wskazał problemów z indeksowalnością ani podstawowymi elementami strony.
Technicznie istotne dla widoczności okazały się trzy rzeczy:
- Doświadczenie na telefonie. Google indeksuje strony w wersji mobilnej, a są częścią sygnałów jakości strony (page experience). Rozjazd między 99 a 66 punktami i 6,9 s w teście mobilnym to główny problem, na który wskazywał audyt.
- Treść musi zostać w HTML. Każda zmiana wydajnościowa, czy to przeniesienie układu do CSS, leniwe ładowanie, czy wydzielenie FAQ, była sprawdzana pod kątem tego, czy pełna treść nadal trafia do statycznego HTML. Innymi słowy, nie chowałem niczego za JavaScriptem.
- Interpretacja danych terenowych. CrUX pokazywał dobre terenowe LCP i podwyższony na telefonach (p75 0,11 przy progu 0,1, a na komputerach 0,02), ale dla całej domeny, a nie dla strony głównej. Dobre wyniki LCP z danych terenowych nie unieważniają testów laboratoryjnych, a z kolei wysoki CLS nie oznacza automatycznie, że jego źródłem jest strona główna. Obie te sprawy wymagają dodatkowej weryfikacji.
Diagnoza wydajności: PageSpeed Insights, HAR i element LCP
Raport PSI wskazywał za duże obrazy, nieużywany JavaScript, CSS blokujący renderowanie i słaby cache. Nie było na tym etapie jasne, który element faktycznie opóźnia stronę, ale wiedziałem, w jakich obszarach szukać. Dalsza weryfikacja w przeglądarce potwierdziła, że elementem LCP na stronie głównej był główny obraz (hero image), natomiast analiza pliku HAR ujawniła szczegółową strukturę pobrań zasobów:
- 78 żądań, łącznie około 1,41 MiB transferu,
- JavaScript — niemal 66% tego transferu,
- 45 żądań (ok. 444 KiB, prawie 31% transferu) powiązanych z prefetchem innych podstron,
- 6 kafelków mapy (ok. 291 KiB) plus kod Leaflet i jego arkusz stylów.
Dzięki analizie pliku HAR odrzuciłem zbyt prostą hipotezę: skoro główny obraz kończył pobieranie po ok. 822 ms od startu nawigacji, a kafelki mapy startowały dopiero po ok. 1,45 s, mapa nie opóźniała ładowania elementu LCP w tej sesji, chociaż generowała dodatkowy koszt podczas początkowego ładowania. Z uwagi na to, że plik HAR nie rejestruje momentu renderowania elementów ani obciążenia procesora, dane te zestawiłem z analizą kodu oraz pomiarami Lighthouse, zamiast opierać wnioski wyłącznie na czasach transferu zasobów.
Problemy z wydajnością i kolejność poprawek
Kolejność prac ustaliłem na podstawie dwóch rzeczy: jak duży koszt dany problem generował w pomiarach i czy dotyczył bezpośrednio pierwszego widoku na telefonie.
| Problem | Dowód | Wpływ | Priorytet |
|---|---|---|---|
| Prefetch tras, których użytkownik nie wybrał | HAR: 45 żądań, ~444 KiB, ~31% transferu | Transfer i praca JS przy wejściu | Wysoki |
Główny obraz bez wariantów srcset | Kod + HTML z HAR: brak srcset dla 4 obrazów | Element LCP pobierany w pełnym rozmiarze | Wysoki |
| Mapa ładowana niezależnie od przewinięcia | HAR: 6 kafelków ~291 KiB + Leaflet | Transfer przy wejściu | Średni |
| Przełączanie układu z desktopowego na mobilny przez JS | Przegląd kodu kart i FAQ | Dodatkowy render, ryzyko przesunięć | Średni |
| Import pełnej bazy FAQ na stronie głównej | Przegląd kodu | Rozmiar paczki JS | Niski |
| Google Analytics ładowane wcześnie | Przegląd kodu | Praca JS na starcie | Niski |
| Jednodniowy cache wersjonowanych zasobów | HAR: krótki cache z rewalidacją | Kolejne wizyty | Niski (dla pierwszego ładowania) |
Wdrożenie 1: ograniczenie prefetchu w Next.js
Ograniczenie automatycznego prefetchu dla kart podstron oraz linku do bloga w nawigacji to nie wszystko — dalsze pomiary wykazały, że zasoby nadal pobierały się m.in. dla linku do polityki prywatności w banerze zgód czy przycisków „Dodaj swoją firmę”. Dopiero powiązanie pobranych fragmentów z manifestem paczek JavaScriptu pozwoliło precyzyjnie przypisać je do konkretnych tras i objąć optymalizacją.
Prefetchu nie wyłączyłem w całym projekcie, lecz wyłącznie tam, gdzie pomiary potwierdziły realny koszt podczas wejścia (linki działają bez zmian). To doskonały przykład tego, dlaczego ostrzeżenie o „nieużywanym JavaScripcie” bywa mylące: kod wcale nie był zbędny w serwisie, lecz po prostu pobierał się przedwcześnie.
Na tym prefetch się nie skończył. Nagranie HAR wykonane trzy dni po pierwszym wdrożeniu pokazało, że stare trasy zniknęły, ale pojawiła się nowa grupa: 36 żądań i ok. 225 KiB dla sześciu stron usług elektrycznych. Nowego kodu nie było — po uproszczeniu animacji (Wdrożenie 4) sekcja z linkami w opisie usług elektrycznych przesunęła się do pierwszego widoku, a Next.js pobiera z wyprzedzeniem strony dla widocznych linków. W drugim etapie wyłączyłem prefetch dla wszystkich 16 linków w opisach usług na stronie głównej. W kolejnym nagraniu prefetch to już jedno żądanie o wadze 0,5 KiB.
Wdrożenie 2: srcset, WebP i AVIF przy eksporcie statycznym Next.js
W konfiguracji Next.js ustawione było images.unoptimized: true, co jest zgodne z eksportem statycznym, ale w tej konfiguracji komponent next/image nie generuje zestawu srcset, a samo podanie sizes niczego nie zmienia. W rezultacie zdjęcie wyglądało poprawnie na telefonie, ale przeglądarka pobierała plik przygotowany dla większego ekranu, więc nie było to optymalne rozwiązanie. Dodałem generowanie wariantów przed kompilacją, używając skryptu opartego na Sharp — przygotowuje mniejsze pliki i zapisuje ich adresy w manifeście, a strona serwuje je przez <picture> i srcset.
Wprowadzone zmiany:
Zdjęcia w kartach dostały rozmiary WebP, a główny obraz dodatkowo AVIF i osobny kadr mobilny.
Oryginalny główny obraz ważył ok. 91 KiB, a mobilny kadr 768 px waży ok. 48 KiB w WebP i 28 KiB w AVIF.
Preload głównego obrazu używa tych samych wariantów i warunków
mediaco właściwy obraz, więc przeglądarka nie pobiera równolegle wersji mobilnej i desktopowej.Przeglądarki bez AVIF dostają WebP, a obrazy poniżej pierwszego widoku zachowały leniwe ładowanie.
Telefony nie zawsze pobierają wariant 768 px — przy ekranach o wyższej gęstości pikseli przeglądarka może wybrać większą grafikę. Emulowany Moto G Power, którego używa Lighthouse, pobiera kadr 768 px (28 KiB), a Pixel 7 z gęstszym ekranem kadr 960 px (34 KiB). Dlatego weryfikowałem faktycznie pobrany adres URL obrazu, a nie wyłącznie obecność odpowiednich atrybutów w kodzie HTML. Szerzej o wyborze między next/image, srcset a CDN piszę w poradniku optymalizacji obrazów w Next.js.
Wdrożenie 3: leniwe ładowanie mapy Leaflet po przewinięciu
Dynamiczny import mapy chronił dotychczas wyłącznie przed renderowaniem po stronie serwera, uruchamiając komponent od razu po jego zamontowaniu. Wdrożenie mechanizmu opartego na obserwacji widoczności (IntersectionObserver z marginesem 200 px) uzależniło ładowanie mapy, jej stylów i kafelków od zbliżania się sekcji do obszaru widocznego.
Dzięki zarezerwowaniu stałej wysokości kontenera wyeliminowałem skoki układu (CLS), a opcjonalny przycisk pozwala na ręczne włączenie widoku. Rozwiązanie wdrożyłem na stronie głównej, podstronach miejskich, w katalogu oraz sekcjach usługowych. Brak sztucznych opóźnień czasowych sprawia, że elementy znajdujące się blisko pierwszego widoku ładowane są natychmiast.
Wdrożenie 4: układ mobilny w CSS zamiast przełączania w JavaScripcie
Część komponentów — m.in. karty usług i typografia FAQ — renderowała się najpierw w wariancie desktopowym, a po uruchomieniu JS wykrywała szerokość ekranu i przełączała się na mobilny. To dodatkowy render i potencjalna zmiana odstępów, widocznego tekstu i geometrii strony już po jej wyświetleniu.
Przeniosłem te różnice do punktów przełamania w CSS. Karty mają jedno drzewo HTML, a układ jest właściwy od pierwszego renderu. Mobilne rozwijanie opisów zostało, a pełna treść jest w HTML. Usunąłem wejściowe rozmywanie i przesuwanie kart oraz efekt paralaksy. W selektorze usług pierwszy krok jest widoczny od razu, animacje między krokami zostały.
Nie przypisuję tej zmianie naprawy CLS. Laboratoryjne przesunięcia były małe już przed zmianami, a terenowy CLS dotyczył całej domeny.
Wdrożenie 5: lżejsze FAQ, Google Analytics dopiero po zgodzie i dostępność
FAQ. Strona główna pokazuje 10 pytań, ale importowała całą bazę. Wydzieliłem używaną sekcję do osobnego modułu. Pełna strona FAQ korzysta z tych samych danych, więc nie powstały dwie kopie do synchronizacji.
Google Analytics. W pierwszym etapie biblioteka zaczęła się ładować przez
lazyOnload, czyli później, ale nadal przy każdej wizycie. W drugim etapiegtag.js(ok. 178 KiB) pobiera się dopiero po zgodzie na analitykę. Bez zgody biblioteka wysyłała wyłącznie anonimowe sygnały trybu zgody (), a mimo to obciążała procesor przy każdej pierwszej wizycie i w każdym teście Lighthouse. Ta zmiana dotyczy już zakresu pomiaru: wizyty bez zgody nie trafiają do GA4 nawet jako sygnały do modelowania, więc liczba sesji w raportach może spaść.Dostępność. Poprawiłem nazwę dostępną linku logo i kontrast części linków w opisach usług, więc Ułatwienia dostępu wróciły do 100 punktów w automatycznym audycie.
Wdrożenie 6: długi cache zasobów i bezpieczne kolejne wdrożenia
Wersjonowane zasoby miały jednodniowy cache z obowiązkową rewalidacją, bo reguła dłuższego cache dla plików Next.js przegrywała z regułami ogólnymi. Teraz wersjonowane fragmenty kodu i obrazy dostają roczny cache z immutable, a HTML i manifesty krótszy, bo pod stałym adresem może zmienić się treść. Skrypt wdrożenia zachowuje starsze wersje obrazów, żeby HTML z cache przeglądarki nie odwoływał się do usuniętych plików. Korzyści dotyczą kolejnych wizyt, więc nie wliczam ich do wyników poniżej.
Wyniki przed zmianami i po nich: Lighthouse, HAR i PageSpeed Insights
- transferu strony głównej w nagraniu HAR (z 1446 do 506 KiB)
- −65%transferu strony głównej w nagraniu HAR (z 1446 do 506 KiB)
- punkty wydajności na telefonie w PageSpeed Insights
- 66 → 94punkty wydajności na telefonie w PageSpeed Insights
- mobilnego LCP w PageSpeed Insights
- 6,9 → 2,85 smobilnego LCP w PageSpeed Insights
- żądań prefetch przy wejściu na stronę główną
- 45 → 1żądań prefetch przy wejściu na stronę główną
Lokalne pomiary Lighthouse
Przed wdrożeniem sprawdzałem zmiany na lokalnym eksporcie w Lighthouse 13.5.0, po trzy przebiegi na wariant. Liczba żądań spadła tam z 76 do 28 na telefonie i z 89 do 28 na komputerze, a wyniki przestały skakać: przed zmianami przebiegi dawały od 68 do 88 punktów, po zmianach wszystkie trzy 88. Lokalny serwer nie odtwarzał hostingu produkcyjnego, więc o efektach rozstrzygają pomiary z produkcji poniżej.
Nagrania HAR na produkcji: przed zmianami i po nich
Lokalny audyt nie pokazuje wszystkiego, co dzieje się na produkcji, dlatego stronę główną nagrałem jeszcze dwa razy: po pierwszym etapie wdrożeń (Wdrożenia 1–6) i po drugim, czyli po wyłączeniu nowego prefetchu i zmianie ładowania Google Analytics. Pierwsze dwa nagrania pochodzą z Chrome DevTools, trzecie z Chrome sterowanego skryptem: okno 1440×900, wyłączony cache, bez przewijania, 7 s od startu nawigacji, trzy przebiegi o identycznej liczbie żądań i bajtów.
| Komputer | Przed zmianami | Po 1. etapie | Po 2. etapie |
|---|---|---|---|
| Żądania | 78 | 62 | 23 |
| Transfer | 1446 KiB | 982 KiB | 506 KiB |
| JavaScript | 956 KiB | 666 KiB | 275 KiB |
| Prefetch | 45 żądań / 444 KiB | 36 żądań / 225 KiB | 1 żądanie / 0,5 KiB |
| Kafelki mapy i Leaflet | 295 KiB | 0 | 0 |
gtag.js | 178 KiB | 178 KiB | 0 |
Na telefonie (emulacja Pixel 7) strona po drugim etapie pobiera 22 żądania i 403 KiB, a główny obraz w AVIF waży 34 KiB.
Jest jedno zastrzeżenie: nagrania z DevTools nie zapisują rozmiaru okna, a od niego zależy, które linki i leniwie ładowane obrazy trafią do pomiaru. W nagraniu po pierwszym etapie pobrały się trzy zdjęcia kart usług (152 KiB), a przy oknie 1440×900 pobiera się tylko jedno z nich (79 KiB). Część różnicy między drugą a trzecią kolumną wynika więc z wysokości okna, a nie z poprawek. Spadki prefetchu, mapy i gtag.js nie zależą od rozmiaru okna, bo potwierdziłem je także przy oknie 1440×1300.
Wyniki w PageSpeed Insights na produkcji
Po każdym etapie zmierzyłem stronę główną w PageSpeed Insights, po trzy niezależne przebiegi na urządzenie. PSI przez chwilę zwraca zapisany wynik dla tego samego adresu, więc kolejne przebiegi uruchamiałem z parametrem w URL i sprawdzałem, czy każdy ma inny czas pomiaru.
| Telefon | Przed zmianami (1 przebieg) | Po 1. etapie (mediana z 3) | Po 2. etapie (mediana z 3) |
|---|---|---|---|
| Wydajność | 66 | 84 (81–94) | 94 (91–95) |
| FCP | 2,3 s | 1,45 s | 1,05 s |
| LCP | 6,9 s | 2,90 s | 2,85 s |
| TBT | 240 ms | 340 ms | 116 ms |
| Speed Index | 5,1 s | 4,60 s | 2,88 s |
| CLS | 0 | 0 | 0 |
Na komputerze wynik po drugim etapie to 99 punktów (96–100), LCP 0,53 s i 100 ms. Pozostałe kategorie nadal mają po 100 punktów na obu urządzeniach. Na produkcji potwierdziłem też nagłówki: wersjonowane fragmenty kodu dostają roczny cache z immutable, a HTML godzinny z rewalidacją.
Mobilny LCP spadł po pierwszym etapie z 6,9 do 2,9 s i w drugim już się nie zmienił (2,85–2,89 s we wszystkich przebiegach), bo usunięte w nim zasoby pobierały się dopiero po wyświetleniu głównego obrazu. Wzrost wyniku z 84 do 94 punktów to więc efekt mniejszej pracy JavaScriptu na starcie, a nie szybszego głównego obrazu: TBT spadł z 340 do 116 ms, Speed Index z 4,6 do 2,9 s, a nieużywany JavaScript ze 141 do 69 KiB.
Z porównaniem do stanu wyjściowego trzeba być ostrożnym, bo 66 punktów i LCP 6,9 s to pojedynczy pomiar. Lokalne przebiegi „przed” pokazały, że jeden pomiar potrafi różnić się od kolejnych nawet dwukrotnie, więc część spadku LCP może wynikać z niekorzystnego przebiegu wyjściowego.
Testy funkcjonalne po optymalizacji wydajności
Mniejszy transfer nic nie daje, jeśli użytkownik nie może wybrać usługi albo widzi pustą mapę. Dlatego po pierwszym etapie sprawdzałem także działanie interfejsu:
- 34 kontrole w Chromium przy szerokościach 390, 768 i 1366 px — wszystkie przeszły. Objęły wybór miasta i usługi, przejście do wyników, mapę po przewinięciu, rozwijanie opisów i FAQ, zgody, zdarzenia Oferteo, strony elektryczne, gazowe, miejskie, artykuł blogowy i formularz rezerwacji.
- Pełna kompilacja: 786 stron, lint, TypeScript i 96 testów jednostkowych bez błędów.
- Osobne testy dla reguł cache i zachowania plików między wdrożeniami.
Po drugim etapie powtórzyłem TypeScript, lint i 96 testów jednostkowych, a efekt na produkcji sprawdziłem w nagraniach HAR i PSI opisanych wyżej.
Czego nie osiągnąłem: mobilny LCP i kolejne kroki
Cel na telefonach miał dwie części: 90 punktów i LCP do 2,5 s. Pierwszą osiągnąłem dopiero po drugim etapie (mediana 94 punkty), drugiej nie. LCP 2,85 s jest wprawdzie daleko od wyjściowych 6,9 s, ale nadal w przedziale „wymaga poprawy”.
Z czego składa się 2,85 s mobilnego LCP
Żeby to sprawdzić, nagrałem cztery przebiegi w panelu wydajności Chrome z emulacją telefonu Moto G Power (tego samego, którego używa Lighthouse), siecią Slow 4G i czterokrotnie spowolnionym procesorem. LCP wyszedł 2,82–2,94 s, czyli tyle co w PSI.
| Część LCP | Czas | Udział |
|---|---|---|
| Odpowiedź serwera (TTFB) | ok. 145 ms | 5% |
| Opóźnienie startu pobierania obrazu | ok. 550 ms | 21% |
| Pobieranie obrazu | ok. 2100 ms | 72% |
| Opóźnienie renderu | 25–50 ms | 1% |
Głównym źródłem problemu jest przepustowość łącza na początku ładowania. Obraz jest przygotowany poprawnie, bo jest w HTML od pierwszego bajtu, ma fetchpriority="high" i nie ma leniwego ładowania. Startuje jednak w tej samej chwili co prawie cały pierwszy widok — zanim się wyświetli, przeglądarka zaczyna pobierać 391 z 397 KiB strony. Wysoki priorytet niewiele tu zmienia, bo wszystkie te pliki dzielą jedno łącze, więc obraz ważący 28 KiB pobiera się ponad 2 s.
Przegląd kodu z mapami źródeł pokazał, co dzieli łącze z obrazem:
Biblioteki animacji i menu. framer-motion waży ok. 49 KiB po kompresji, a Headless UI z zależnościami ok. 34 KiB, czyli razem więcej niż sam React. Obsługują menu mobilne, rozwijane menu na komputerze, przejścia między krokami selektora usług i efekty wciśnięcia przycisków, czyli głównie rzeczy, które użytkownik uruchamia dopiero kliknięciem.
Duplikaty. Katalog usług z next-seo (11,6 KiB) i runtime (4,1 KiB) trafiają do HTML w dwóch identycznych plikach. Build przez webpack nie pomaga, bo strona główna ma wtedy 277 zamiast 270 KiB JavaScriptu.
Fonty. Plus Jakarta Sans to font zmienny, więc jeden plik obsługuje wszystkie grubości, ale strona pobiera dwa: podstawowy zestaw łaciński (27 KiB) i rozszerzony z polskimi literami (21,5 KiB). Konfiguracja
next/fontdeklaruje tylkolatin, więc drugi plik nie ma preloadu i przeglądarka odkrywa go dopiero po przetworzeniu CSS, tuż przed LCP.
Przy przepustowości Slow 4G (ok. 195 KiB/s) każde ok. 70 KiB mniej przed wyświetleniem obrazu to ok. 0,35 s, czyli mniej więcej tyle, ile brakuje do progu.
Następne kroki:
- Zastąpienie framer-motion przejściami CSS w nagłówku, przyciskach i selektorze usług, a menu mobilnego i rozwijanego natywnymi elementami albo kodem ładowanym dopiero po pierwszym kliknięciu (do ok. 80 KiB mniej przed LCP).
- Usunięcie zduplikowanych plików JavaScriptu (ok. 16 KiB).
- Jeden plik fontu z polskimi znakami, ładowany wstępnie (ok. 15–20 KiB mniej i brak późnego odkrywania).
- Obserwacja terenowego CLS na telefonach w kolejnych 28-dniowych oknach CrUX, z rozbiciem na typy stron. Pierwszy sygnał wygląda dobrze, bo CLS dla domeny spadł z 0,11 do 0,10, ale okno nadal obejmuje głównie dni sprzed wdrożeń i trzeba czekać do końca listopada na jego przesunięcie.
Jak wygląda strona usługowa w Next.js doprowadzona do 100 punktów na obu urządzeniach, pokazuję w case study PageSpeed 100/100.


