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 buildKiedy 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:
- SSG - najszybsza opcja, idealna dla statycznych treści
- ISR - kompromis między szybkością a aktualnością danych
- SSR - pełna dynamiczność kosztem wydajności
- 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}