Pagespeed 100/100 w Next.js — case study optymalizacji strony usługowej

Jak osiągnąć wynik 100/100 w PageSpeed Insights dla strony Next.js? Case study optymalizacji LCP, INP i CLS — fonty, obrazy, JavaScript, third-party scripts i Server Components.

Opublikowano

10 kwietnia 2026 14:40

Czytanie

4 min czytania

Aktualizacja

15 kwietnia 2026 11:52

Punkt wyjścia: typowa strona usługowa

Mamy tu typową stronę usługową zbudowaną w Next.js, wyposażoną w Google Analytics, Google Tag Manager, prosty formularz kontaktowy, galerię zdjęć i animacje – czyli standardowy zestaw. Na początku testy PageSpeed Insights pokazały wyniki: 67/100 na urządzeniach mobilnych i 89/100 na komputerach stacjonarnych.

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, 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.

Krok 1: Fonty — eliminacja layout shift

Problem stanowiły Google Fonts ładowane z CDN powodowały FOUT i CLS.

Rozwiązaniem było użycie next/font z self-hostingiem 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 requestu 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 element) ładował się z opóźnieniem, brak sizes powodował pobieranie zbyt dużych wariantów.

Code
<Image
  src={heroImage}
  alt="Strona internetowa Next.js"
  priority // Najwyższy priorytet dla LCP image
  placeholder="blur"
  sizes="100vw"
  className="h-auto w-full"
/>

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.

Krok 3: Third-party scripts — lazy loading GTM

Problemem okazał się Google Tag Manager (130 KB), który blokował rendering. GTM ładowany w <head> opóźniał FCP 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">
      <body>
        {children}
        <GoogleTagManager gtmId="GTM-XXXXX" />
      </body>
    </html>
  )
}

@next/third-parties automatycznie opóźnia ładowanie GTM i skrypt nie blokuje renderingu.

Dodatkowa optymalizacja — ładowanie GTM po interakcji użytkownika:

Code
'use client'
 
import Script from 'next/script'
 
export function LazyGTM() {
  return (
    <Script
      id="gtm"
      strategy="lazyOnload" // Ładuj po onload
      src={`https://www.googletagmanager.com/gtm.js?id=GTM-XXXXX`}
    />
  )
}

W wyniku tych działań TBT (Total Blocking Time) spadł z 420 ms do 80 ms.

Krok 4: JavaScript bundle — minimalizacja Client Components

Krok 4: Optymalizacja pakietu JavaScript – 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 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>:

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 6: CSS — 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 7: Animacje bez blokowania

Framer Motion ładowane lazy, ale tylko na stronach z animacjami:

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

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);
}

Wynik końcowy

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

Checklist optymalizacji

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

  • next/font dla wszystkich fontów (z latin-ext dla polskiego),
  • next/image z priority na LCP element i sizes na 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.

Podsumowanie

100/100 w PageSpeed da się osiągnąć w warunkach laboratoryjnych, ale większe znaczenie mają stabilnie dobre Core Web Vitals na produkcji. Największe zyski w Next.js dają: self-hosted fonty (CLS → 0), atrybut priority na LCP image, lazy loading third-party scripts i minimalizacja Client Components.

Nie musisz rezygnować z funkcjonalności, ale musisz ładować je w sposób logiczny i przemyślany.

Najczęściej zadawane pytania

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?

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

Pracuję z tym zawodowo.

Jeśli chcesz połączyć SEO, analitykę, Google Ads i warstwę techniczną strony w jeden sensowny system wzrostu, skontaktuj się ze mną. Pomagam układać wdrożenia, które nie kończą się na samym tagowaniu, ale wspierają widoczność, pomiar i konwersję.

O autorze

Maciej Sala

Maciej Sala — project manager i frontendowiec z doświadczeniem w marketingu internetowym. Na co dzień pracuję z Reactem, Next.js i TypeScriptem, łącząc perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat związany z branżą gier wideo jako project manager i game designer.

Absolwent historii na Uniwersytecie Jagiellońskim i studiów podyplomowych z marketingu internetowego na Akademii Górniczo-Hutniczej w Krakowie. Poza pracą trenuje na siłowni, maluje figurki i realizuje własne projekty.

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów
Astro.js vs Next.js — które narzędzie wybrać w 2026 roku?

Astro.js vs Next.js — które narzędzie wybrać w 2026 roku?

Fachowe porównanie Astro.js i Next.js z perspektywy developera pracującego na co dzień w Next.js. Architektura, wydajność, SEO, DX, koszty i konkretne use case — z benchmarkami i przykładami kodu.

Maciej Sala

Maciej Sala

Founder Strivelab