Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

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

W Metropolii Quantum, inżynierowie nieustannie monitorują i optymalizują wydajność wszystkich systemów miejskich - od sieci energetycznych po transportowe. Każda decyzja architektoniczna ma wpływ na szybkość i efektywność całego miasta. Podobnie, w Next.js 15, wybór odpowiedniej strategii renderowania ma fundamentalny wpływ na wydajność aplikacji.

W tym rozdziale poznasz szczegółowe porównanie strategii renderowania, metryki wydajności i praktyczne przykłady, które pomogą ci podejmować świadome decyzje architektoniczne.

Trzy główne strategie renderowania w Next.js 15

1. SSR - Server-Side Rendering (Renderowanie po stronie serwera)

Jak działa: Każde żądanie użytkownika powoduje renderowanie strony na serwerze w czasie rzeczywistym.

1// app/dashboard/page.tsx
2// Domyślnie Next.js 15 używa cache, więc musimy jawnie wyłączyć
3export const dynamic = 'force-dynamic';
4// LUB
5// fetch(..., { cache: 'no-store' })
6
7async function getDashboardData() {
8  const res = await fetch('https://api.quantum.com/dashboard', {
9    cache: 'no-store' // Wymusza SSR
10  });
11  return res.json();
12}
13
14export default async function Dashboard() {
15  const data = await getDashboardData();
16
17  return (
18    <div>
19      <h1>Panel kontrolny</h1>
20      <p>Ostatnia aktualizacja: {new Date().toLocaleTimeString()}</p>
21      <DashboardContent data={data} />
22    </div>
23  );
24}

Zalety:

  • ✅ Zawsze świeże dane
  • ✅ Doskonałe dla personalizowanej zawartości
  • ✅ Dynamiczna zawartość bez opóźnień

Wady:

  • ❌ Wolniejszy TTFB (Time To First Byte)
  • ❌ Większe obciążenie serwera
  • ❌ Może być droższy w hostingu

2. SSG - Static Site Generation (Generowanie statyczne)

Jak działa: Strony są generowane raz podczas build time i serwowane jako statyczne pliki HTML.

1// app/blog/[slug]/page.tsx
2
3async function getPost(slug: string) {
4  const res = await fetch(`https://api.quantum.com/posts/${slug}`, {
5    cache: 'force-cache' // Wymusza SSG
6  });
7  return res.json();
8}
9
10export async function generateStaticParams() {
11  const posts = await fetch('https://api.quantum.com/posts').then(res => res.json());
12
13  return posts.map((post) => ({
14    slug: post.slug,
15  }));
16}
17
18export default async function BlogPost({
19  params,
20}: {
21  params: Promise<{ slug: string }>;
22}) {
23  const post = await getPost((await params).slug);
24
25  return (
26    <article>
27      <h1>{post.title}</h1>
28      <time>{post.publishedAt}</time>
29      <div dangerouslySetInnerHTML={{ __html: post.content }} />
30    </article>
31  );
32}

Zalety:

  • ✅ Bardzo szybki TTFB
  • ✅ Niskie koszty hostingu (CDN)
  • ✅ Doskonałe dla SEO
  • ✅ Najlepsza wydajność

Wady:

  • ❌ Dane mogą być przestarzałe
  • ❌ Długi czas build dla dużych witryn
  • ❌ Wymaga rebuild przy każdej zmianie

3. ISR - Incremental Static Regeneration (Przyrostowe odświeżanie statyczne)

Jak działa: Strony są generowane statycznie, ale mogą być odświeżane w tle po określonym czasie.

1// app/products/[id]/page.tsx
2
3async function getProduct(id: string) {
4  const res = await fetch(`https://api.quantum.com/products/${id}`, {
5    next: { revalidate: 3600 } // Rewalidacja co godzinę
6  });
7  return res.json();
8}
9
10export async function generateStaticParams() {
11  // Generuj tylko najpopularniejsze produkty podczas build
12  const popularProducts = await fetch('https://api.quantum.com/products/popular')
13    .then(res => res.json());
14
15  return popularProducts.map((product) => ({
16    id: product.id.toString(),
17  }));
18}
19
20export default async function ProductPage({
21  params,
22}: {
23  params: Promise<{ id: string }>;
24}) {
25  const product = await getProduct((await params).id);
26
27  return (
28    <div>
29      <h1>{product.name}</h1>
30      <p>Cena: {product.price} QC</p>
31      <p>Dostępność: {product.stock} sztuk</p>
32      <button>Dodaj do koszyka</button>
33    </div>
34  );
35}
36
37export const revalidate = 3600; // Globalny czas rewalidacji

Zalety:

  • ✅ Kompromis między SSR a SSG
  • ✅ Szybki TTFB jak SSG
  • ✅ Zawartość odświeżana automatycznie
  • ✅ Strony generowane na żądanie (on-demand)

Wady:

  • ❌ Pierwsi użytkownicy mogą widzieć stare dane
  • ❌ Wymaga konfiguracji czasu rewalidacji
  • ❌ Bardziej złożona logika cache

Porównanie metryk wydajności

TTFB - Time To First Byte

TTFB to czas od wysłania żądania do otrzymania pierwszego bajta odpowiedzi. W Metropolii Quantum to jak czas reakcji systemu na polecenie - im szybszy, tym lepiej.

Benchmark (przykładowe wartości):

| Strategia | TTFB | Ocena | |-----------|------|-------| | SSG | 50-100ms | ⭐⭐⭐⭐⭐ Doskonały | | ISR (cached) | 50-150ms | ⭐⭐⭐⭐ Bardzo dobry | | ISR (revalidating) | 200-500ms | ⭐⭐⭐ Dobry | | SSR | 300-800ms | ⭐⭐ Średni | | SSR (slow API) | 1000ms+ | ⭐ Słaby |

1// Pomiar TTFB w przeglądarce
2performance.getEntriesByType('navigation')[0].responseStart;

LCP - Largest Contentful Paint

LCP mierzy czas do wyświetlenia największego elementu treści. To jak szybkość pojawiania się głównego hologramu w Metropolii Quantum.

Benchmark:

| Strategia | LCP | Ocena | |-----------|-----|-------| | SSG | 0.5-1.5s | ⭐⭐⭐⭐⭐ Doskonały | | ISR | 0.8-2s | ⭐⭐⭐⭐ Bardzo dobry | | SSR | 1.5-3s | ⭐⭐⭐ Dobry | | SSR (heavy) | 3s+ | ⭐⭐ Wymaga optymalizacji |

Rekomendacje Google:

  • Dobry: < 2.5s
  • Wymaga poprawy: 2.5s - 4s
  • Słaby: > 4s
1// Monitoring LCP
2import { onLCP } from 'web-vitals';
3
4onLCP((metric) => {
5  console.log('LCP:', metric.value);
6  // Wysłanie do analytics
7  analytics.track('Web Vitals', {
8    metric: 'LCP',
9    value: metric.value,
10    rating: metric.rating
11  });
12});

CLS - Cumulative Layout Shift

CLS mierzy wizualną stabilność strony. To jak stabilność holograficznych interfejsów w Metropolii Quantum - nie powinny "skakać".

Benchmark:

| Strategia | CLS | Ocena | |-----------|-----|-------| | SSG | 0-0.05 | ⭐⭐⭐⭐⭐ Doskonały | | ISR | 0-0.1 | ⭐⭐⭐⭐ Bardzo dobry | | SSR | 0.1-0.25 | ⭐⭐⭐ Dobry (jeśli dobrze zoptymalizowane) |

Rekomendacje Google:

  • Dobry: < 0.1
  • Wymaga poprawy: 0.1 - 0.25
  • Słaby: > 0.25
1// Monitoring CLS
2import { onCLS } from 'web-vitals';
3
4onCLS((metric) => {
5  console.log('CLS:', metric.value);
6  analytics.track('Web Vitals', {
7    metric: 'CLS',
8    value: metric.value,
9    rating: metric.rating
10  });
11});

Kiedy używać której strategii?

Wybierz SSG gdy:

Blog lub strona marketingowa

  • Treść zmienia się rzadko
  • SEO jest priorytetem
  • Potrzebujesz najlepszej wydajności
1// app/blog/[slug]/page.tsx - Idealny dla SSG
2export async function generateStaticParams() {
3  const posts = await getPosts();
4  return posts.map(post => ({ slug: post.slug }));
5}
6
7export default async function BlogPost({ params }) {
8  const post = await getPost((await params).slug);
9  return <Article post={post} />;
10}

Strona dokumentacji

  • Treść statyczna lub rzadko aktualizowana
  • Wielojęzyczność
  • Szybkość ładowania krytyczna

Landing pages

  • Marketing campaigns
  • Produkty/usługi
  • Wysoka konwersja = szybkie ładowanie

Wybierz ISR gdy:

E-commerce (strony produktów)

  • Dane zmieniają się regularnie (ceny, stock)
  • Duża liczba produktów
  • Balance między świeżością a wydajnością
1// app/products/[id]/page.tsx - Idealny dla ISR
2export const revalidate = 300; // 5 minut
3
4export default async function Product({ params }) {
5  const product = await getProduct((await params).id);
6
7  return (
8    <div>
9      <h1>{product.name}</h1>
10      <p>Cena: {product.price}</p>
11      <p>W magazynie: {product.stock}</p>
12    </div>
13  );
14}

Newsroom / Media

  • Artykuły aktualizowane kilka razy dziennie
  • Dużo treści
  • Potrzeba szybkości + świeżości

Profile użytkowników (publiczne)

  • Dane zmieniają się rzadko
  • Duża liczba profili
  • Potrzeba cache z odświeżaniem

Wybierz SSR gdy:

Dashboard / Panel administracyjny

  • Dane w czasie rzeczywistym
  • Personalizacja
  • Autoryzacja użytkownika
1// app/dashboard/page.tsx - Idealny dla SSR
2export const dynamic = 'force-dynamic';
3
4export default async function Dashboard() {
5  const session = await getServerSession();
6  const userData = await getUserData(session.userId);
7
8  return (
9    <div>
10      <h1>Witaj, {userData.name}</h1>
11      <RealTimeAnalytics userId={session.userId} />
12    </div>
13  );
14}

Search results

  • Dynamiczne zapytania
  • Personalizowane wyniki
  • Zawsze świeże dane

User-specific content

  • Koszyk zakupowy
  • Preferencje użytkownika
  • Historia aktywności

Real-world przykłady z benchmarkami

Przykład 1: Blog technologiczny (SSG)

1// app/blog/[slug]/page.tsx
2import { Metadata } from 'next';
3
4export async function generateMetadata({ params }): Promise<Metadata> {
5  const post = await getPost((await params).slug);
6
7  return {
8    title: post.title,
9    description: post.excerpt,
10    openGraph: {
11      images: [post.coverImage],
12    },
13  };
14}
15
16export async function generateStaticParams() {
17  const posts = await getAllPosts();
18
19  return posts.map((post) => ({
20    slug: post.slug,
21  }));
22}
23
24async function getPost(slug: string) {
25  const res = await fetch(`https://cms.quantum.com/posts/${slug}`, {
26    cache: 'force-cache'
27  });
28  return res.json();
29}
30
31export default async function BlogPost({ params }) {
32  const post = await getPost((await params).slug);
33
34  return (
35    <article>
36      <h1>{post.title}</h1>
37      <time>{post.publishedAt}</time>
38      <Image
39        src={post.coverImage}
40        alt={post.title}
41        width={1200}
42        height={630}
43        priority
44      />
45      <div dangerouslySetInnerHTML={{ __html: post.content }} />
46    </article>
47  );
48}

Metryki:

  • TTFB: ~80ms
  • LCP: ~1.2s
  • CLS: 0.02
  • Build time: 2min dla 500 postów
  • Ocena: ⭐⭐⭐⭐⭐

Przykład 2: E-commerce marketplace (ISR)

1// app/products/[id]/page.tsx
2
3async function getProduct(id: string) {
4  const res = await fetch(`https://api.quantum-shop.com/products/${id}`, {
5    next: { revalidate: 600 } // Rewalidacja co 10 minut
6  });
7
8  if (!res.ok) {
9    notFound();
10  }
11
12  return res.json();
13}
14
15export async function generateStaticParams() {
16  // Generuj tylko top 100 produktów podczas build
17  const topProducts = await fetch('https://api.quantum-shop.com/products/top?limit=100')
18    .then(res => res.json());
19
20  return topProducts.map((product) => ({
21    id: product.id.toString(),
22  }));
23}
24
25export default async function ProductPage({ params }) {
26  const product = await getProduct((await params).id);
27
28  return (
29    <div className="product-page">
30      <ProductGallery images={product.images} />
31
32      <div className="product-info">
33        <h1>{product.name}</h1>
34
35        <div className="price-stock">
36          <span className="price">{product.price} QC</span>
37          <StockIndicator stock={product.stock} />
38        </div>
39
40        <div className="description">
41          {product.description}
42        </div>
43
44        <AddToCartButton productId={product.id} />
45
46        <ProductReviews productId={product.id} />
47      </div>
48
49      <RelatedProducts categoryId={product.categoryId} />
50    </div>
51  );
52}
53
54export const revalidate = 600; // 10 minut

Metryki:

  • TTFB: ~120ms (cached), ~400ms (revalidating)
  • LCP: ~1.8s
  • CLS: 0.05
  • Build time: 5min dla 100 top produktów
  • On-demand generation: ~500ms dla pozostałych
  • Ocena: ⭐⭐⭐⭐

Przykład 3: Real-time dashboard (SSR)

1// app/dashboard/analytics/page.tsx
2
3export const dynamic = 'force-dynamic';
4export const fetchCache = 'force-no-store';
5
6async function getRealTimeAnalytics(userId: string) {
7  const res = await fetch(`https://api.quantum.com/analytics/${userId}`, {
8    cache: 'no-store',
9    headers: {
10      Authorization: `Bearer ${process.env.API_TOKEN}`
11    }
12  });
13
14  return res.json();
15}
16
17export default async function AnalyticsDashboard() {
18  const session = await getServerSession();
19
20  if (!session) {
21    redirect('/login');
22  }
23
24  const analytics = await getRealTimeAnalytics(session.user.id);
25
26  return (
27    <div className="analytics-dashboard">
28      <h1>Analityka w czasie rzeczywistym</h1>
29
30      <div className="stats-grid">
31        <StatCard
32          title="Aktywni użytkownicy"
33          value={analytics.activeUsers}
34          change={analytics.activeUsersChange}
35        />
36
37        <StatCard
38          title="Przychody dzisiaj"
39          value={`${analytics.todayRevenue} QC`}
40          change={analytics.revenueChange}
41        />
42
43        <StatCard
44          title="Konwersje"
45          value={`${analytics.conversionRate}%`}
46          change={analytics.conversionChange}
47        />
48      </div>
49
50      <RevenueChart data={analytics.revenueChart} />
51      <UserActivityFeed data={analytics.recentActivity} />
52    </div>
53  );
54}

Metryki:

  • TTFB: ~450ms
  • LCP: ~2.4s
  • CLS: 0.08
  • Świeżość danych: Zawsze aktualne
  • Ocena: ⭐⭐⭐ (dobry dla real-time, ale wymaga optymalizacji)

Przykład 4: Hybrid approach - Newsfeed (ISR + Client-side updates)

1// app/news/page.tsx
2
3async function getLatestNews() {
4  const res = await fetch('https://api.quantum-news.com/latest', {
5    next: { revalidate: 60 } // Rewalidacja co minutę
6  });
7
8  return res.json();
9}
10
11export default async function NewsFeed() {
12  const initialNews = await getLatestNews();
13
14  return (
15    <div className="news-feed">
16      <h1>Wiadomości z Metropolii Quantum</h1>
17
18      {/* Server-rendered initial content (ISR) */}
19      <NewsList initialNews={initialNews} />
20
21      {/* Client-side updates co 30 sekund */}
22      <ClientNewsUpdater />
23    </div>
24  );
25}
26
27export const revalidate = 60;
1// components/ClientNewsUpdater.tsx
2'use client';
3
4import { useEffect, useState } from 'react';
5
6export function ClientNewsUpdater() {
7  const [latestNews, setLatestNews] = useState([]);
8
9  useEffect(() => {
10    const interval = setInterval(async () => {
11      const res = await fetch('/api/news/latest');
12      const data = await res.json();
13      setLatestNews(data);
14    }, 30000); // 30 sekund
15
16    return () => clearInterval(interval);
17  }, []);
18
19  if (latestNews.length === 0) return null;
20
21  return (
22    <div className="breaking-news">
23      <h2>Najnowsze wiadomości</h2>
24      {latestNews.map(news => (
25        <NewsCard key={news.id} news={news} />
26      ))}
27    </div>
28  );
29}

Metryki:

  • Initial TTFB: ~150ms
  • LCP: ~1.5s
  • CLS: 0.04
  • Świeżość: ISR co minutę + client updates co 30s
  • Ocena: ⭐⭐⭐⭐⭐ (najlepsze z obu światów)

Narzędzia do monitorowania wydajności

1. Web Vitals w Next.js 15

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

2. Custom Web Vitals reporting

1// app/web-vitals.tsx
2'use client';
3
4import { useReportWebVitals } from 'next/web-vitals';
5
6export function WebVitals() {
7  useReportWebVitals((metric) => {
8    // Wysyłanie do własnego analytics
9    const body = JSON.stringify({
10      name: metric.name,
11      value: metric.value,
12      rating: metric.rating,
13      delta: metric.delta,
14      id: metric.id,
15    });
16
17    fetch('/api/analytics/web-vitals', {
18      method: 'POST',
19      headers: { 'Content-Type': 'application/json' },
20      body,
21    });
22  });
23
24  return null;
25}

3. Lighthouse CI w pipeline

1# .github/workflows/lighthouse.yml
2name: Lighthouse CI
3
4on:
5  pull_request:
6    branches: [main]
7
8jobs:
9  lighthouse:
10    runs-on: ubuntu-latest
11    steps:
12      - uses: actions/checkout@v3
13      - uses: actions/setup-node@v3
14
15      - name: Install dependencies
16        run: npm ci
17
18      - name: Build
19        run: npm run build
20
21      - name: Run Lighthouse CI
22        run: |
23          npm install -g @lhci/cli
24          lhci autorun
25
26      - name: Upload results
27        uses: actions/upload-artifact@v3
28        with:
29          name: lighthouse-results
30          path: .lighthouseci

Optymalizacja dla każdej strategii

SSG Optimizations

1// next.config.js
2module.exports = {
3  images: {
4    formats: ['image/avif', 'image/webp'],
5    deviceSizes: [640, 750, 828, 1080, 1200],
6  },
7  compress: true,
8  poweredByHeader: false,
9  generateEtags: true,
10};

ISR Optimizations

1// Użyj on-demand revalidation dla ważnych aktualizacji
2// app/api/revalidate/route.ts
3import { revalidatePath } from 'next/cache';
4import { NextRequest } from 'next/server';
5
6export async function POST(request: NextRequest) {
7  const secret = request.nextUrl.searchParams.get('secret');
8
9  if (secret !== process.env.REVALIDATION_TOKEN) {
10    return Response.json({ message: 'Invalid token' }, { status: 401 });
11  }
12
13  const path = request.nextUrl.searchParams.get('path');
14
15  if (path) {
16    revalidatePath(path);
17    return Response.json({ revalidated: true });
18  }
19
20  return Response.json({ message: 'Missing path' }, { status: 400 });
21}

SSR Optimizations

1// Użyj streaming dla szybszego TTFB
2// app/dashboard/page.tsx
3import { Suspense } from 'react';
4
5export default function Dashboard() {
6  return (
7    <div>
8      <h1>Dashboard</h1>
9
10      <Suspense fallback={<AnalyticsSkeleton />}>
11        <Analytics />
12      </Suspense>
13
14      <Suspense fallback={<ChartSkeleton />}>
15        <RevenueChart />
16      </Suspense>
17    </div>
18  );
19}

Podsumowanie i Decision Tree

1Czy dane muszą być w czasie rzeczywistym?
2├─ TAK → SSR
3│   └─ Rozważ streaming i Suspense dla lepszej wydajności
45└─ NIE
67    ├─ Czy dane zmieniają się częściej niż raz na godzinę?
8    │   ├─ TAK → ISR (revalidate: 60-3600)
9    │   │   └─ Rozważ on-demand revalidation dla ważnych aktualizacji
10    │   │
11    │   └─ NIE → SSG
12    │       └─ Użyj generateStaticParams dla wszystkich możliwych ścieżek
1314    └─ Czy potrzebujesz personalizacji?
15        ├─ TAK → SSR lub ISR + Client Components
16        └─ NIE → SSG lub ISR

Kluczowe wnioski:

  1. SSG - najszybszy, ale statyczny (blogi, marketing, dokumentacja)
  2. ISR - balance (e-commerce, news, public profiles)
  3. SSR - zawsze świeży (dashboards, search, user content)
  4. Hybrid - często najlepsze rozwiązanie (ISR + Client-side updates)

Podobnie jak inżynierowie Metropolii Quantum optymalizują każdy system dla maksymalnej wydajności, tak i ty powinieneś świadomie wybierać strategię renderowania dla każdej części aplikacji. Nie ma jednego "najlepszego" rozwiązania - klucz to dopasowanie strategii do konkretnego przypadku użycia.

Monitoruj Web Vitals, testuj różne podejścia i zawsze stawiaj doświadczenie użytkownika na pierwszym miejscu. W erze kwantowych technologii, każda millisekunda ma znaczenie!

Przejdź do CodeWorlds