Punkt wyjścia: typowa strona usługowa w Next.js
Lighthouse wskazało główne problemy: duże zasoby JavaScript (GTM) blokujące renderowanie, niezoptymalizowane obrazy, brak odpowiednich wskazówek dla przeglądarki (resource hints) oraz nadmierne przesunięcia układu strony (layout shift) związane z ładowaniem czcionek.
Naszym celem było poprawienie rzeczywistych wskaźników , a przy okazji osiągnięcie jak najwyższego wyniku w testach laboratoryjnych Lighthouse. Wynik 100/100 zarówno na urządzeniach mobilnych, jak i desktopowych jest możliwy do uzyskania, ale nie powinien być celem za wszelką cenę. Generalnie uzyskanie powyżej 90/100 w zupełności wystarcza.
- PageSpeed Mobile po optymalizacji
- 67 → 100PageSpeed Mobile po optymalizacji
- LCP po uporządkowaniu obrazów i fontów
- 3.2 s → 1.1 sLCP po uporządkowaniu obrazów i fontów
- JavaScript bundle po ograniczeniu Client Components
- 210 KB → 85 KBJavaScript bundle po ograniczeniu Client Components
Od 2026 do rozmowy o PageSpeed dochodzi jeszcze Przeglądanie Agentowe. Ten audyt nie zastępuje Performance i nie ma klasycznej skali 0–100, ale sprawdza rzeczy, które w takim projekcie są istotne, czyli accessibility tree, CLS, llms.txt i eksperymentalne WebMCP. Dlatego 100/100 w Performance traktuj jako jeden z wymiarów jakości, który nie oznacza końca diagnostyki.
Krok 1: fonty w Next.js i eliminacja layout shift
Problem stanowiły Google Fonts ładowane z powodowały FOUT i .
Rozwiązaniem było użycie next/font z samodzielnym hostowaniem i size-adjust:
W wyniku tych działań wyeliminowaliśmy zewnętrzny request do fonts.googleapis.com i uzyskaliśmy spadek CLS z fontów z 0.12 do 0.00.
Krok 2: obrazy w Next.js, next/image, preload i sizes
Problem stanowił obraz hero image ( element) ładował się z opóźnieniem, brak sizes powodował pobieranie zbyt dużych wariantów.
Rozwiązaniem jest użycie next/image, dodanie preload tylko na obraz LCP widoczny na pierwszym ekranie oraz ustawienie sizes zgodnie z realnym rozmiarem elementu w layoucie. Next.js dobiera zoptymalizowany wariant obrazu i może serwować WebP albo AVIF zależnie od konfiguracji images.formats oraz nagłówka Accept przeglądarki. To ważne, bo sizes="100vw" przy obrazie, który na desktopie ma 1200 px szerokości, potrafi wymusić pobranie za dużego pliku.
Jeśli projekt działa jeszcze na starszej wersji Next.js, możesz spotkać priority, ale w Next.js 16 właściwą nazwą jest preload. Semantyka jest czytelniejsza: chodzi o wcześniejsze pobranie krytycznego obrazu, a nie o podbijanie priorytetu każdego obrazka na stronie.
W wyniku tych działań LCP spadł z 3.2 s do 1.4 s (a po komplecie optymalizacji z kolejnych kroków zszedł do 1.1 s).
Krok 3: third-party scripts i lazy loading GTM
Problemem okazał się (130 KB), który blokował rendering. GTM ładowany w <head> opóźniał o 800 ms.
Rozwiązanie: @next/third-parties z opóźnionym ładowaniem:
@next/third-parties pobiera oryginalny skrypt GTM po hydratacji strony, więc nie wchodzi w krytyczną ścieżkę pierwszego renderu. Trzeba jednak pamiętać, że według dokumentacji Next.js ta biblioteka jest nadal eksperymentalna i aktywnie rozwijana, dlatego przy większych wdrożeniach warto sprawdzić jej zachowanie po aktualizacjach frameworka.
Alternatywnie, jeśli nie korzystasz z @next/third-parties, podobny efekt osiągniesz ręcznie przez next/script. Dla tagów analitycznych, które muszą działać wcześnie, zwykle wybierasz afterInteractive; dla czatów, widgetów społecznościowych i skryptów niskiego priorytetu lepsze jest lazyOnload, bo ładuje je dopiero w czasie bezczynności przeglądarki:
W prawdziwej implementacji nie kopiuj bezrefleksyjnie połowy snippetów GTM. Jeśli zależy Ci na zdarzeniach przed pełnym załadowaniem kontenera, zachowaj inicjalizację dataLayer i sprawdź, czy konfiguracja Consent Mode nie opóźnia albo nie dubluje zdarzeń. Wydajność i poprawność analityki trzeba mierzyć razem, bo szybka strona z uszkodzonym pomiarem konwersji nadal jest problemem biznesowym.
W wyniku tych działań spadł z 420 ms do 80 ms (po minimalizacji Client Components w kroku 4 zszedł finalnie do 60 ms).
Krok 4: JavaScript bundle i minimalizacja Client Components
Zauważyliśmy, że w projekcie było zbyt wiele Client Components. Przykładowo, sekcja hero, nawigacja i stopka były oznaczone jako 'use client', choć w rzeczywistości nie wymagały interaktywności po stronie klienta.
Rozwiązaniem było przekształcenie tych elementów na Server Components wszędzie tam, gdzie było to możliwe. Client Components zostały zachowane wyłącznie dla funkcjonalności wymagających faktycznej interakcji, takich jak przełącznik menu mobilnego, formularz kontaktowy czy animacje związane ze scrollowaniem.
W wyniku tych optymalizacji JavaScript zmalał z 210 KB do 85 KB (gzipped).
Krok 5: INP i długie zadania po stronie klienta
Samo zmniejszenie bundle'a nie gwarantuje dobrego . Możesz mieć niewielki JavaScript, który nadal blokuje główny wątek, jeśli ciężka logika odpala się dokładnie po kliknięciu użytkownika: walidacja dużego formularza, filtrowanie listy, inicjalizacja mapy, edytor rich text albo rozbudowana biblioteka UI.
W tym projekcie sprawdziliśmy interakcje, które realnie wykonuje użytkownik: otwarcie menu, wysłanie formularza, przejście przez galerię i kliknięcie CTA. Zasada była prosta: handler kliknięcia ma robić minimum pracy, a cięższe rzeczy mają być przeniesione poza krytyczny moment interakcji.
Nie każda interakcja wymaga startTransition, ale warto znać ten mechanizm, gdy aktualizacja widoku może być odczuwalna. Przy prostych elementach lepiej wygrać jeszcze wcześniej: zastąpić JavaScript stanem CSS, natywnym <details>, prostym formularzem HTML albo mniejszym komponentem klienckim.
Krok 6: preconnect i DNS prefetch dla zasobów zewnętrznych
Metadata nie służy do dowolnego generowania tagów preconnect i dns-prefetch, więc najprościej dodać je bezpośrednio w <head>:
Krok 7: CSS i eliminacja nieużywanych klas
Tailwind CSS z konfiguracją content automatycznie tree-shakuje nieużywane klasy. Upewnij się, że ścieżki są poprawne:
Wynikowy CSS: ~12 KB (gzipped) zamiast pełnych ~300 KB Tailwind.
Krok 8: animacje bez blokowania Core Web Vitals
Framer Motion ładowane lazy, ale tylko na stronach z animacjami dekoracyjnymi albo niekrytycznymi:
ssr: false nie jest uniwersalnym sposobem na performance. Jeśli animowana sekcja zawiera ważny tekst, CTA, zdjęcie LCP albo treść istotną dla SEO, nie wyłączaj jej renderowania po stronie serwera. W takim przypadku zostaw HTML w Server Component, a do klienta przenieś wyłącznie minimalny fragment odpowiedzialny za animację.
Micro-interactions (hover, tap) zastąpione czystym CSS:
Krok 9: TTFB, cache i hosting produkcyjny
Lighthouse lokalnie nie mierzy tego, co często psuje wynik na produkcji, czyli wolnego , zimnych startów, błędnej konfiguracji CDN i braku cache dla statycznych assetów. W Next.js szczególnie ważne jest rozróżnienie, które trasy są statyczne, które dynamiczne, a z kolei które mogą korzystać z cache.
Dla strony usługowej najczęściej chcesz, żeby publiczne podstrony marketingowe były statyczne albo mocno cache'owane. Dynamiczne renderowanie powinno być tam, gdzie sytuacja zależy od żądania, czyli personalizacji, sesji, cookies albo danych zmiennych per użytkownik.
Na Vercel część assetów Next.js ma sensowne nagłówki domyślnie, ale własne katalogi statyczne, fonty, grafiki marketingowe i pliki tekstowe nadal warto sprawdzić w odpowiedzi HTTP. Jeśli hostujesz Next.js samodzielnie na VPS, dochodzi jeszcze kompresja Brotli/Gzip, reverse proxy i cache na poziomie CDN.
Krok 10: monitoring po deployu i budżety wydajności
Jednorazowe osiągnięcie 100/100 w PageSpeed nie chroni przed regresją, bo wystarczy dodać nowy widget, dodatkowy tag marketingowy, cięższy hero image albo niepozorny Client Component dodany do layoutu. Dlatego po optymalizacji warto dodać minimalny system kontroli:
- PageSpeed Insights albo Lighthouse CI dla najważniejszych szablonów stron.
useReportWebVitalslub narzędzie RUM do śledzenia LCP, INP i CLS po deployu.- Budżet JavaScriptu dla głównych tras, np. ostrzeżenie po przekroczeniu ustalonego rozmiaru bundle'a.
- Test po każdej zmianie GTM, Consent Mode, czatu, mapy albo skryptu remarketingowego.
- Porównywanie wyników na tym samym typie strony i tym samym środowisku, zamiast mieszać localhost, preview i produkcję.
Czy 100/100 oznacza wszystkie kategorie Lighthouse?
W tym case study chodzi przede wszystkim o kategorię Performance w PageSpeed Insights, ale trzeba przy tym pamiętać, że Lighthouse ma więcej kategorii: Accessibility, Best Practices, SEO oraz coraz częściej eksperymentalne audyty, takie jak Przeglądanie Agentowe. W związku z powyższym, wynik Performance 100/100 nie powinien być skrótem myślowym oznaczającym, że cała strona została całkowicie zoptymalizowana.
Przy finalnym odbiorze projektu sprawdzam osobno semantyczne nagłówki, kontrast, teksty alternatywne obrazów, poprawne metadane, canonical, statusy HTTP, formularze, politykę bezpieczeństwa i dostępność elementów interaktywnych w accessibility tree. Performance jest ważny, ale to tylko jedna warstwa jakości technicznej.
Wynik końcowy: PageSpeed 100/100 w Next.js
Podsumowując wszystkie działania optymalizacyjne dobiliśmy do poniższych wyników:
| Metryka | Przed | Po |
|---|---|---|
| PageSpeed Mobile | 67 | 100 |
| PageSpeed Desktop | 89 | 100 |
| LCP | 3.2 s | 1.1 s |
| INP | 180 ms | 45 ms |
| CLS | 0.12 | 0.00 |
| TBT | 420 ms | 60 ms |
| JS bundle | 210 KB | 85 KB |
| CSS | 45 KB | 12 KB |
Lista kontrolna optymalizacji PageSpeed w Next.js
Przed każdym deploymentem, warto skorzystać z tej krótkiej listy kontrolnej:
next/fontdla wszystkich fontów (zlatin-extdla polskiego),next/imagezpreloadna element LCP isizesna każdym obrazie,- Third-party scripts z
strategy="lazyOnload"lub@next/third-parties, - Minimum Client Components — Server Components jako domyślne,
- INP sprawdzane na realnych interakcjach: menu, formularz, galeria, CTA,
- Preconnect do zewnętrznych domen,
- Tailwind z poprawnymi
contentścieżkami, - Animacje CSS zamiast JS dla micro-interactions,
headers()w next.config.ts z cache-control dla statycznych assets,- Monitoring po deployu: Lighthouse CI, PageSpeed Insights albo RUM.
Ten wynik uzyskaliśmy w Next.js, gdzie największy zysk dały next/image z preload, samodzielny hosting fontów, ograniczenie Client Components i kontrola skryptów third-party. Dla stron czysto treściowych, gdzie JavaScript można ograniczyć do minimum, Astro daje podobne lub lepsze efekty przy niższym nakładzie — jak to wygląda w praktyce, opisuję w analogicznym case study dla Astro.
