Punkt wyjścia: typowa strona usługowa
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 Core Web Vitals to zestaw metryk Google oceniających realne doświadczenie użytkownika: LCP (szybkość ładowania), INP (responsywność) i CLS (stabilność wizualna). Wpływają na ranking i konwersję., 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 → 100
- LCP po uporządkowaniu obrazów i fontów
- 3.2 s → 1.1 s
- JavaScript bundle po ograniczeniu Client Components
- 210 KB → 85 KB
Krok 1: Fonty — eliminacja layout shift
Problem stanowiły Google Fonts ładowane z CDN, czyli Content Delivery Network, to rozproszona sieć serwerów dostarczająca zasoby z węzła najbliższego użytkownikowi; CDN do obrazów dodatkowo transformuje je w locie. powodowały FOUT i CLS (Cumulative Layout Shift) to Core Web Vital mierzący nieoczekiwane przesunięcia elementów podczas ładowania i działania strony. Animacje psują go wtedy, gdy ruszają właściwości layoutowe albo gdy pojawiający się element nie ma zarezerwowanego miejsca..
Rozwiązaniem było użycie next/font z self-hostingiem 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 — next/image z priority i sizes
Problem stanowił obraz hero image (LCP, czyli Largest Contentful Paint, mierzy czas do wyrenderowania największego widocznego elementu — oznaczenie go preload przyspiesza jego załadowanie. element) ładował się z opóźnieniem, brak sizes powodował pobieranie zbyt dużych wariantów.
Rozwiązaniem jest konwersja wszystkich obrazów na WebP/AVIF automatycznie przez next/image, dodanie sizes do każdego <Image> z precyzyjnymi breakpointami.
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 — lazy loading GTM
Problemem okazał się GTM, czyli Google Tag Manager, pozwala zarządzać tagami i skryptami marketingowymi bez każdej zmiany w kodzie aplikacji. (130 KB), który blokował rendering. GTM ładowany w <head> opóźniał FCP (First Contentful Paint) to czas do wyrenderowania pierwszego fragmentu treści strony — pierwszy sygnał dla użytkownika, że coś się ładuje. o 800 ms.
Rozwiązanie: @next/third-parties z opóźnionym ładowaniem:
@next/third-parties automatycznie opóźnia ładowanie GTM i skrypt nie blokuje renderingu.
Alternatywnie, jeśli nie korzystasz z @next/third-parties, ten sam efekt osiągniesz ręcznie przez next/script ze strategy="lazyOnload" — GTM ładuje się dopiero po onload:
W wyniku tych działań TBT, czyli Total Blocking Time, to łączny czas, w którym główny wątek jest zablokowany i nie reaguje na interakcje użytkownika podczas ładowania. spadł z 420 ms do 80 ms (po minimalizacji Client Components w kroku 4 zszedł finalnie do 60 ms).
Krok 4: JavaScript bundle — 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 bundle zmalał z 210 KB do 85 KB (gzipped).
Krok 5: Preconnect i DNS prefetch
Metadata API nie służy do dowolnego generowania tagów preconnect i dns-prefetch, więc najprościej dodać je bezpośrednio w <head>:
Krok 6: CSS — 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 7: Animacje bez blokowania
Framer Motion ładowane lazy, ale tylko na stronach z animacjami:
Micro-interactions (hover, tap) zastąpione czystym CSS:
Wynik końcowy
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 |
Checklist optymalizacji
Przed każdym deploymentem, warto skorzystać z tej krótkiej checklisty:
next/fontdla wszystkich fontów (zlatin-extdla polskiego),next/imagezpriorityna LCP element isizesna każdym obrazie,- Third-party scripts z
strategy="lazyOnload"lub@next/third-parties, - Minimum Client Components — Server Components jako domyślne,
- 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.
