Przejdź do treści

Jak osiągnąć 100/100 w Lighthouse Performance w Next.js? Case study

Jak wygląda w realnym projekcie 100/100 w PageSpeed dla strony Next.js? Metryki, decyzje i co wpłynęło na dobry wynik.

Maciej Sala

Founder StriveLab

9 min czytaniaOpublikowano 10 kwietnia 2026 (Aktualizacja 16 lipca 2026)

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:

Code
import { Inter } from 'next/font/google'
 
const inter = Inter({
  subsets: ['latin', 'latin-ext'],
  display: 'swap',
  variable: '--font-inter',
})

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.

Code
<Image
  src={heroImage}
  alt="Strona internetowa Next.js"
  preload // Next.js 16: preload dla obrazu LCP
  placeholder="blur"
  sizes="(max-width: 768px) 100vw, 1200px"
  className="h-auto w-full"
/>

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:

Code
// app/layout.tsx
import { GoogleTagManager } from '@next/third-parties/google'
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="pl">
      <GoogleTagManager gtmId="GTM-XXXXX" />
      <body>{children}</body>
    </html>
  )
}

@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:

Code
'use client'
 
import Script from 'next/script'
 
export function LazyGTM() {
  return (
    <Script
      id="gtm"
      strategy="lazyOnload" // Tylko gdy GTM nie musi startować od razu
      src={`https://www.googletagmanager.com/gtm.js?id=GTM-XXXXX`}
    />
  )
}

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.

Code
/* PRZED — cały hero jako Client Component
'use client'; // ← niepotrzebne
export function HeroSection() { ... } */
 
/* PO — Server Component (zero JS w bundle) */
export function HeroSection() { ... }

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.

Code
'use client'
 
import { startTransition, useState } from 'react'
 
export function GalleryFilter({ items }: { items: { category: string }[] }) {
  const [filteredItems, setFilteredItems] = useState(items)
 
  function handleCategoryChange(category: string) {
    startTransition(() => {
      setFilteredItems(items.filter((item) => item.category === category))
    })
  }
 
  // ...
}

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>:

Code
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="pl">
      <head>
        <link rel="preconnect" href="https://www.googletagmanager.com" />
        <link rel="dns-prefetch" href="https://www.google-analytics.com" />
      </head>
      <body>{children}</body>
    </html>
  )
}

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:

Code
// tailwind.config.ts
const config = {
  content: [
    './app/**/*.{ts,tsx}',
    './components/**/*.{ts,tsx}',
    // NIE: './node_modules/**' — to zaciągnie ogromne CSS
  ],
}

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:

Code
import dynamic from 'next/dynamic'
 
const AnimatedSection = dynamic(() => import('./animated-section'), {
  ssr: false,
  loading: () => <div className="h-96" />,
})

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:

Code
.btn-hover {
  transition: transform 0.15s ease;
}
.btn-hover:hover {
  transform: scale(1.02);
}
.btn-hover:active {
  transform: scale(0.98);
}

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.

Code
// next.config.ts
import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: '/fonts/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
      {
        source: '/images/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
    ]
  },
}
 
export default nextConfig

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:

  1. PageSpeed Insights albo Lighthouse CI dla najważniejszych szablonów stron.
  2. useReportWebVitals lub narzędzie RUM do śledzenia LCP, INP i CLS po deployu.
  3. Budżet JavaScriptu dla głównych tras, np. ostrzeżenie po przekroczeniu ustalonego rozmiaru bundle'a.
  4. Test po każdej zmianie GTM, Consent Mode, czatu, mapy albo skryptu remarketingowego.
  5. 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:

MetrykaPrzedPo
PageSpeed Mobile67100
PageSpeed Desktop89100
LCP3.2 s1.1 s
INP180 ms45 ms
CLS0.120.00
TBT420 ms60 ms
JS bundle210 KB85 KB
CSS45 KB12 KB

Lista kontrolna optymalizacji PageSpeed w Next.js

Przed każdym deploymentem, warto skorzystać z tej krótkiej listy kontrolnej:

  1. next/font dla wszystkich fontów (z latin-ext dla polskiego),
  2. next/image z preload na element LCP i sizes na każdym obrazie,
  3. Third-party scripts z strategy="lazyOnload" lub @next/third-parties,
  4. Minimum Client Components — Server Components jako domyślne,
  5. INP sprawdzane na realnych interakcjach: menu, formularz, galeria, CTA,
  6. Preconnect do zewnętrznych domen,
  7. Tailwind z poprawnymi content ścieżkami,
  8. Animacje CSS zamiast JS dla micro-interactions,
  9. headers() w next.config.ts z cache-control dla statycznych assets,
  10. 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.

Audyt techniczny i optymalizacja pod kątem SEO i GEO.
Audyt techniczny SEO

Często zadawane pytania

Czy wynik 100/100 w PageSpeed zawsze jest celem?

Nie zawsze, ponieważ wynik 100/100 jest dobrym sygnałem technicznym, ale ważniejsze są realne Core Web Vitals, konwersja, stabilność i brak regresji na produkcji.

Co najczęściej psuje PageSpeed w Next.js?

Najczęściej problemy powodują ciężkie skrypty third-party, niezoptymalizowane obrazy, fonty powodujące layout shift, zbyt duży JavaScript i słaba priorytetyzacja głównej treści.

Czy optymalizacja performance kończy się po jednym audycie?

Nie. Performance trzeba monitorować po zmianach treści, dodaniu tagów marketingowych, nowych komponentów i aktualizacjach zależności.

Czy 100/100 naprawdę wpływa na pozycję w Google?

Pośrednio lub w ograniczony sposób. Google bierze pod uwagę page experience, ale nie warto gonić za samym 100/100 kosztem wartości dodanej produktu. Najważniejsze jest utrzymanie dobrych Core Web Vitals i szybkiej, stabilnej strony dla użytkownika, ale nie popadajmy w myślenie, że 100/100 jest celem samym w sobie.

Czy to jednorazowa optymalizacja?

Nie, ponieważ każda nowa biblioteka, obraz czy skrypt third-party może obniżyć wynik. Monitoruj PageSpeed regularnie, a najlepiej w CI/CD pipeline.

Czy wynik 100 na localhost = 100 na produkcji?

Niekoniecznie, ponieważ Lighthouse lokalnie nie mierzy TTFB serwera, opóźnień CDN ani real-user conditions. Zawsze testuj na produkcyjnym URL.

O autorze

Maciej Sala

Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwijam własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy na temat Next.js

Czytaj dalej

Zobacz więcej wpisów
Jak zdobyć 100/100 w Lighthouse z Astro? Case Study

Astro startuje z przewagą, na którą w Next.js trzeba zapracować: domyślnie wysyła zero JavaScriptu, więc świeży projekt często ma Lighthouse powyżej 95 bez jednej optymalizacji. Droga od wyniku „prawie 100” do perfekcyjnego „100/100” w PageSpeed to kwestia detali i dlatego pokazuję, jak zoptymalizować obrazy, fonty oraz skrypty zewnętrzne, i czy gra jest warta świeczki.

Maciej Sala

Maciej Sala

Founder StriveLab

Core Web Vitals — jak przyspieszyć stronę i poprawić pozycję w Google

Google od wielu lat podkreśla, że wydajność i doświadczenie użytkownika mają znaczenie i Core Web Vitals są częścią sygnałów page experience. Najważniejsze jest jednak to, że wreszcie mierzą coś bardzo konkretnego: czy strona błyskawicznie pokazuje treść, czy odpowiednio reaguje na interakcje i czy nie „skacze" nieprzyjemnie podczas ładowania.

Maciej Sala

Maciej Sala

Founder StriveLab

Przeglądanie Agentowe w PageSpeed Insights: jak przygotować stronę pod agentów AI

Przez lata projektowaliśmy strony internetowe dla dwóch odbiorców: użytkownika i Googlebota. Z jednej strony staraliśmy się o czytelny interfejs dla użytkownika, a z drugiej łatwo indeksowalny HTML dla Googlebota. Pojawienie się kategorii Przeglądanie Agentowe w Lighthouse dodaje trzecią perspektywę: agenta AI, który ma nie tylko przeczytać stronę, ale też zrozumieć strukturę, znaleźć właściwy element i czasem wykonać akcję.

Maciej Sala

Maciej Sala

Founder StriveLab