CodeWorlds

Performance Benchmarks w Next.js 16 - SSR vs SSG vs ISR

Kurs Next.js · Moduł 5: Strategie renderowania

W Metropolii Quantum roku 2150 wydajność systemów to kwestia przetrwania. Każda milisekunda opóźnienia może oznaczać utratę danych w kwantowej sieci lub awarię krytycznych systemów. Dlatego inżynierowie Metropolii muszą dokładnie rozumieć różnice wydajnościowe między strategiami renderowania w Next.js 16 i umieć wybrać optymalne rozwiązanie dla każdego przypadku.

Czym są benchmarki wydajności?

Benchmarki to pomiary wydajności aplikacji w kontrolowanych warunkach. Pozwalają porównać różne strategie renderowania pod kątem kluczowych metryk, takich jak:

  • TTFB (Time to First Byte) - czas od wysłania żądania do otrzymania pierwszego bajtu odpowiedzi
  • FCP (First Contentful Paint) - czas do wyrenderowania pierwszej treści na ekranie
  • LCP (Largest Contentful Paint) - czas do wyrenderowania największego elementu w widoku
  • TTI (Time to Interactive) - czas do pełnej interaktywności strony
  • CLS (Cumulative Layout Shift) - stabilność wizualna strony podczas ładowania

Porównanie strategii renderowania

SSG (Static Site Generation) - najszybsza opcja

SSG generuje strony w czasie budowania aplikacji. Wynikowe pliki HTML są serwowane bezpośrednio z CDN, co zapewnia najniższe możliwe opóźnienia.

1// app/blog/[slug]/page.tsx - SSG w Next.js 16
2import { notFound } from 'next/navigation';
3
4// Generowanie statycznych ścieżek w czasie budowania
5export async function generateStaticParams() {
6  const posts = await fetch('https://api.example.com/posts').then(r => r.json());
7  return posts.map((post: { slug: string }) => ({ slug: post.slug }));
8}
9
10// Bez API czasu żądania strona jest prerenderowana przy budowaniu (SSG).
11// Od Next.js 15 fetch nie jest domyślnie cachowany, więc cache włączamy jawnie
12async function getPost(slug: string) {
13  const res = await fetch(`https://api.example.com/posts/${slug}`, { cache: 'force-cache' });
14  if (!res.ok) return null;
15  return res.json();
16}
17
18export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
19  const post = await getPost((await params).slug);
20  if (!post) notFound();
21
22  return (
23    <article>
24      <h1>{post.title}</h1>
25      <p>{post.content}</p>
26    </article>
27  );
28}

Typowe metryki SSG:

| Metryka | Wartość | Ocena | |---------|---------|-------| | TTFB | 5-50ms | Doskonała | | FCP | 100-300ms | Doskonała | | LCP | 200-500ms | Doskonała | | TTI | 300-800ms | Bardzo dobra | | CLS | 0-0.05 | Doskonała |

SSR (Server-Side Rendering) - dynamiczna zawartość

SSR renderuje strony na serwerze przy każdym żądaniu. Jest wolniejsze niż SSG, ale zapewnia zawsze aktualne dane.

1// app/dashboard/page.tsx - SSR w Next.js 16
2import { cookies } from 'next/headers';
3
4// Wymuszenie dynamicznego renderowania
5export const dynamic = 'force-dynamic';
6
7async function getDashboardData(token: string) {
8  const res = await fetch('https://api.example.com/dashboard', {
9    headers: { Authorization: `Bearer ${token}` },
10    cache: 'no-store', // Brak cachowania = SSR
11  });
12  return res.json();
13}
14
15export default async function Dashboard() {
16  const cookieStore = await cookies();
17  const token = cookieStore.get('auth-token')?.value || '';
18  const data = await getDashboardData(token);
19
20  return (
21    <div>
22      <h1>Panel użytkownika</h1>
23      <p>Saldo: {data.balance} QC</p>
24      <p>Ostatnia aktywność: {data.lastActivity}</p>
25    </div>
26  );
27}

Typowe metryki SSR:

| Metryka | Wartość | Ocena | |---------|---------|-------| | TTFB | 100-500ms | Średnia | | FCP | 300-800ms | Dobra | | LCP | 500-1200ms | Średnia | | TTI | 500-1500ms | Średnia | | CLS | 0-0.1 | Dobra |

ISR (Incremental Static Regeneration) - kompromis

ISR łączy zalety SSG (szybkość) z SSR (aktualność danych). Strony są generowane statycznie, ale odświeżane w tle po upływie określonego czasu.

1// app/products/page.tsx - ISR w Next.js 16
2async function getProducts() {
3  const res = await fetch('https://api.example.com/products', {
4    next: { revalidate: 60 }, // Rewalidacja co 60 sekund
5  });
6  return res.json();
7}
8
9export default async function ProductsPage() {
10  const products = await getProducts();
11
12  return (
13    <div>
14      <h1>Produkty Quantum Store</h1>
15      <div className="grid">
16        {products.map((product: any) => (
17          <div key={product.id}>
18            <h2>{product.name}</h2>
19            <p>Cena: {product.price} QC</p>
20          </div>
21        ))}
22      </div>
23    </div>
24  );
25}

Typowe metryki ISR:

| Metryka | Wartość (cache hit) | Wartość (rewalidacja) | Ocena | |---------|--------------------|-----------------------|-------| | TTFB | 5-50ms | 100-400ms | Bardzo dobra | | FCP | 100-300ms | 300-700ms | Bardzo dobra | | LCP | 200-500ms | 500-1000ms | Dobra | | TTI | 300-800ms | 500-1200ms | Dobra | | CLS | 0-0.05 | 0-0.05 | Doskonała |

On-Demand Revalidation w Next.js 16

Rewalidacja na żądanie pozwala odświeżyć cache bez czekania na upłynięcie czasu rewalidacji. Next.js 16 zmienił jej API: revalidateTag przyjmuje teraz drugi argument, czyli profil cacheLife (zalecany 'max' oznacza, że użytkownik dostaje starą wersję, a świeża ładuje się w tle), a wersja z jednym argumentem jest przestarzała. W Server Actions do natychmiastowego odświeżenia służy nowa funkcja updateTag:

1// app/api/revalidate/route.ts
2import { revalidatePath, revalidateTag } from 'next/cache';
3import { NextRequest, NextResponse } from 'next/server';
4
5export async function POST(request: NextRequest) {
6  const { path, tag, secret } = await request.json();
7
8  // Weryfikacja tokenu bezpieczeństwa
9  if (secret !== process.env.REVALIDATION_SECRET) {
10    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
11  }
12
13  if (tag) {
14    // Rewalidacja po tagu - odświeża wszystkie fetche z danym tagiem
15    revalidateTag(tag, 'max');
16    return NextResponse.json({ revalidated: true, tag });
17  }
18
19  if (path) {
20    // Rewalidacja ścieżki - odświeża konkretną stronę
21    revalidatePath(path);
22    return NextResponse.json({ revalidated: true, path });
23  }
24
25  return NextResponse.json({ error: 'Missing path or tag' }, { status: 400 });
26}
27
28// Użycie tagów w fetch
29async function getProduct(id: string) {
30  const res = await fetch(`https://api.example.com/products/${id}`, {
31    next: {
32      revalidate: 3600, // Cache na 1 godzinę
33      tags: [`product-${id}`, 'products'], // Tagi do rewalidacji
34    },
35  });
36  return res.json();
37}

Hybrid Rendering - mieszanie strategii

W Next.js 16 można łączyć różne strategie renderowania w ramach jednej strony dzięki Partial Prerendering (PPR). Wymaga ono włączenia cacheComponents: true: fragment z dyrektywą 'use cache' trafia wtedy do statycznej powłoki, a dynamiczny, owinięty w <Suspense>, jest dosyłany przy żądaniu. Bez tej opcji ten sam kod również streamuje dane, ale cała trasa renderuje się przy każdym żądaniu:

1// app/store/page.tsx - Hybrid approach
2import { Suspense } from 'react';
3import { cacheLife } from 'next/cache';
4
5// Statyczna część strony (SSG)
6function StaticHeader() {
7  return (
8    <header>
9      <h1>Quantum Store</h1>
10      <nav>
11        <a href="/products">Produkty</a>
12        <a href="/categories">Kategorie</a>
13      </nav>
14    </header>
15  );
16}
17
18// Dynamiczna część strony (SSR)
19async function DynamicCart() {
20  const res = await fetch('https://api.example.com/cart', { cache: 'no-store' });
21  const cart = await res.json();
22
23  return (
24    <div>
25      <h2>Twój koszyk ({cart.items.length} produktów)</h2>
26      <p>Suma: {cart.total} QC</p>
27    </div>
28  );
29}
30
31// Część cachowana (dawniej ISR) - trafia do statycznej powłoki
32async function ProductList() {
33  'use cache';
34  cacheLife({ revalidate: 300 });
35
36  const res = await fetch('https://api.example.com/featured');
37  const products = await res.json();
38
39  return (
40    <div>
41      {products.map((p: any) => (
42        <div key={p.id}>{p.name} - {p.price} QC</div>
43      ))}
44    </div>
45  );
46}
47
48export default function StorePage() {
49  return (
50    <div>
51      <StaticHeader />
52      <Suspense fallback={<p>Ładowanie koszyka...</p>}>
53        <DynamicCart />
54      </Suspense>
55      <Suspense fallback={<p>Ładowanie produktów...</p>}>
56        <ProductList />
57      </Suspense>
58    </div>
59  );
60}

Jak mierzyć wydajność w Next.js 16?

Web Vitals Reporting

1// app/layout.tsx
2import { SpeedInsights } from '@vercel/speed-insights/next';
3import { Analytics } from '@vercel/analytics/react';
4
5export default function RootLayout({ children }: { children: React.ReactNode }) {
6  return (
7    <html lang="pl">
8      <body>
9        {children}
10        <SpeedInsights />
11        <Analytics />
12      </body>
13    </html>
14  );
15}

Wbudowany profiler Next.js

1# Budowanie z profilowaniem
2NEXT_TELEMETRY_DEBUG=1 next build
3
4# Analiza rozmiaru bundla
5ANALYZE=true next build

Kiedy użyć której strategii?

| Scenariusz | Strategia | Dlaczego? | |------------|-----------|-----------| | Blog, dokumentacja | SSG | Treści się nie zmieniają często | | E-commerce (lista produktów) | ISR (60-300s) | Produkty się zmieniają, ale nie co sekundę | | Dashboard użytkownika | SSR | Dane muszą być zawsze aktualne | | Strona główna z promocjami | ISR (300-3600s) | Promocje zmieniają się co kilka godzin | | Panel administracyjny | SSR | Wymaga aktualnych danych i autoryzacji | | Landing page | SSG | Maksymalna szybkość, statyczna treść | | Feed społecznościowy | SSR + Streaming | Dynamiczne dane, progresywne ładowanie |

Podsumowanie

Wybór strategii renderowania w Next.js 16 powinien być podyktowany konkretnymi wymaganiami projektu:

  1. SSG - najszybsza opcja, idealna dla statycznych treści
  2. ISR - kompromis między szybkością a aktualnością danych
  3. SSR - pełna dynamiczność kosztem wydajności
  4. Hybrid (PPR) - przyszłość renderowania, łączący wszystkie strategie

W Metropolii Quantum 2150 inżynierowie opierają swoje decyzje na danych - mierzą, analizują i optymalizują. Benchmarki wydajności są kluczowym narzędziem w tym procesie.

Kod do tej lekcji: App.tsx
1import React, { useState } from 'react';
2
3// Performance Benchmarks - SSR vs SSG vs ISR
4
5interface BenchmarkResult {
6  strategy: string;
7  ttfb: number;
8  fcp: number;
9  lcp: number;
10  tti: number;
11  cls: number;
12  description: string;
13  useCase: string;
14}
15
16const benchmarks: BenchmarkResult[] = [
17  {
18    strategy: 'SSG (Static Site Generation)',
19    ttfb: 25,
20    fcp: 180,
21    lcp: 350,
22    tti: 500,
23    cls: 0.02,
24    description: 'Strony generowane w czasie budowania. Serwowane z CDN.',
25    useCase: 'Blog, dokumentacja, landing page',
26  },
27  {
28    strategy: 'ISR (Incremental Static Regeneration)',
29    ttfb: 30,
30    fcp: 200,
31    lcp: 400,
32    tti: 550,
33    cls: 0.03,
34    description: 'Statyczne strony odswiezane w tle po okreslonym czasie.',
35    useCase: 'E-commerce, katalogi produktow',
36  },
37  {
38    strategy: 'SSR (Server-Side Rendering)',
39    ttfb: 280,
40    fcp: 550,
41    lcp: 900,
42    tti: 1100,
43    cls: 0.08,
44    description: 'Strony renderowane na serwerze przy kazdym zadaniu.',
45    useCase: 'Dashboard, panel uzytkownika',
46  },
47];
48
49function MetricBar({ value, max, label, unit }: { value: number; max: number; label: string; unit: string }) {
50  const pct = (value / max) * 100;
51  const color = pct < 33 ? '#4caf50' : pct < 66 ? '#ff9800' : '#f44336';
52  return (
53    <div style={{ marginBottom: 8 }}>
54      <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, color: '#aaa', marginBottom: 2 }}>
55        <span>{label}</span>
56        <span style={{ color }}>{value}{unit}</span>
57      </div>
58      <div style={{ height: 8, background: '#1a2744', borderRadius: 4, overflow: 'hidden' }}>
59        <div style={{ width: pct + '%', height: '100%', background: color, borderRadius: 4, transition: 'width 0.6s ease' }} />
60      </div>
61    </div>
62  );
63}
64
65export default function BenchmarkDemo() {
66  const [selected, setSelected] = useState(0);
67  const b = benchmarks[selected];
68
69  return (
70    <div style={{ background: '#0f0f23', minHeight: '100vh', padding: 20, color: '#fff', fontFamily: 'monospace' }}>
71      <h1 style={{ color: '#64ffda', fontSize: 20, marginBottom: 4 }}>Performance Benchmarks</h1>
72      <p style={{ color: '#888', fontSize: 13, marginBottom: 16 }}>SSR vs SSG vs ISR - porownanie wydajnosci</p>
73
74      <div style={{ display: 'flex', gap: 8, marginBottom: 20, flexWrap: 'wrap' }}>
75        {benchmarks.map((bm, i) => (
76          <button key={i} onClick={() => setSelected(i)} style={{
77            padding: '8px 14px', border: '1px solid ' + (i === selected ? '#64ffda' : '#334'),
78            background: i === selected ? '#1a3a4a' : '#151530', color: i === selected ? '#64ffda' : '#aaa',
79            borderRadius: 6, cursor: 'pointer', fontSize: 12,
80          }}>
81            {bm.strategy.split(' ')[0]}
82          </button>
83        ))}
84      </div>
85
86      <div style={{ background: '#151530', borderRadius: 8, padding: 16, marginBottom: 16 }}>
87        <h2 style={{ color: '#64ffda', fontSize: 16, marginBottom: 4 }}>{b.strategy}</h2>
88        <p style={{ color: '#888', fontSize: 12, marginBottom: 12 }}>{b.description}</p>
89
90        <MetricBar value={b.ttfb} max={500} label="TTFB (Time to First Byte)" unit="ms" />
91        <MetricBar value={b.fcp} max={1200} label="FCP (First Contentful Paint)" unit="ms" />
92        <MetricBar value={b.lcp} max={1500} label="LCP (Largest Contentful Paint)" unit="ms" />
93        <MetricBar value={b.tti} max={1500} label="TTI (Time to Interactive)" unit="ms" />
94        <MetricBar value={b.cls} max={0.15} label="CLS (Cumulative Layout Shift)" unit="" />
95      </div>
96
97      <div style={{ background: '#151530', borderRadius: 8, padding: 12 }}>
98        <p style={{ color: '#64ffda', fontSize: 12, marginBottom: 4 }}>Najlepsze zastosowanie:</p>
99        <p style={{ color: '#ccc', fontSize: 13 }}>{b.useCase}</p>
100      </div>
101    </div>
102  );
103}

Przydatne artykuły