Kurs Next.js · Moduł 3: Stylowanie i optymalizacja wizualna

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

12 min czytania ·
W tej lekcji7

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

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.

// app/dashboard/page.tsx
// Od Next.js 15 fetch nie jest domyślnie cachowany, ale strona bez API czasu żądania
// i tak zostałaby wyrenderowana przy budowaniu, więc jawnie wymuszamy renderowanie przy każdym żądaniu
export const dynamic = 'force-dynamic';
// LUB
// fetch(..., { cache: 'no-store' })

async function getDashboardData() {
  const res = await fetch('https://api.quantum.com/dashboard', {
    cache: 'no-store' // Wymusza SSR
  });
  return res.json();
}

export default async function Dashboard() {
  const data = await getDashboardData();

  return (
    <div>
      <h1>Panel kontrolny</h1>
      <p>Ostatnia aktualizacja: {new Date().toLocaleTimeString()}</p>
      <DashboardContent data={data} />
    </div>
  );
}

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.

// app/blog/[slug]/page.tsx

async function getPost(slug: string) {
  const res = await fetch(`https://api.quantum.com/posts/${slug}`, {
    cache: 'force-cache' // Wymusza SSG
  });
  return res.json();
}

export async function generateStaticParams() {
  const posts = await fetch('https://api.quantum.com/posts').then(res => res.json());

  return posts.map((post) => ({
    slug: post.slug,
  }));
}

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const post = await getPost((await params).slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <time>{post.publishedAt}</time>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

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.

// app/products/[id]/page.tsx

async function getProduct(id: string) {
  const res = await fetch(`https://api.quantum.com/products/${id}`, {
    next: { revalidate: 3600 } // Rewalidacja co godzinę
  });
  return res.json();
}

export async function generateStaticParams() {
  // Generuj tylko najpopularniejsze produkty podczas build
  const popularProducts = await fetch('https://api.quantum.com/products/popular')
    .then(res => res.json());

  return popularProducts.map((product) => ({
    id: product.id.toString(),
  }));
}

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const product = await getProduct((await params).id);

  return (
    <div>
      <h1>{product.name}</h1>
      <p>Cena: {product.price} QC</p>
      <p>Dostępność: {product.stock} sztuk</p>
      <button>Dodaj do koszyka</button>
    </div>
  );
}

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

StrategiaTTFBOcena
SSG50-100msDoskonały
ISR (cached)50-150msBardzo dobry
ISR (revalidating)200-500msDobry
SSR300-800msŚredni
SSR (slow API)1000ms+Słaby
// Pomiar TTFB w przeglądarce
performance.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:

StrategiaLCPOcena
SSG0.5-1.5sDoskonały
ISR0.8-2sBardzo dobry
SSR1.5-3sDobry
SSR (heavy)3s+Wymaga optymalizacji

Rekomendacje Google:

  • Dobry: < 2.5s
  • Wymaga poprawy: 2.5s - 4s
  • Słaby: > 4s
// Monitoring LCP
import { onLCP } from 'web-vitals';

onLCP((metric) => {
  console.log('LCP:', metric.value);
  // Wysłanie do analytics
  analytics.track('Web Vitals', {
    metric: 'LCP',
    value: metric.value,
    rating: metric.rating
  });
});

CLS - Cumulative Layout Shift

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

Benchmark:

StrategiaCLSOcena
SSG0-0.05Doskonały
ISR0-0.1Bardzo dobry
SSR0.1-0.25Dobry (jeśli dobrze zoptymalizowane)

Rekomendacje Google:

  • Dobry: < 0.1
  • Wymaga poprawy: 0.1 - 0.25
  • Słaby: > 0.25
// Monitoring CLS
import { onCLS } from 'web-vitals';

onCLS((metric) => {
  console.log('CLS:', metric.value);
  analytics.track('Web Vitals', {
    metric: 'CLS',
    value: metric.value,
    rating: metric.rating
  });
});

Kiedy używać której strategii?

Wybierz SSG gdy:

Blog lub strona marketingowa

  • Treść zmienia się rzadko
  • SEO jest priorytetem
  • Potrzebujesz najlepszej wydajności
// app/blog/[slug]/page.tsx - Idealny dla SSG
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map(post => ({ slug: post.slug }));
}

export default async function BlogPost({ params }) {
  const post = await getPost((await params).slug);
  return <Article post={post} />;
}

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ą
// app/products/[id]/page.tsx - Idealny dla ISR
export const revalidate = 300; // 5 minut

export default async function Product({ params }) {
  const product = await getProduct((await params).id);

  return (
    <div>
      <h1>{product.name}</h1>
      <p>Cena: {product.price}</p>
      <p>W magazynie: {product.stock}</p>
    </div>
  );
}

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
// app/dashboard/page.tsx - Idealny dla SSR
export const dynamic = 'force-dynamic';

export default async function Dashboard() {
  const session = await getServerSession();
  const userData = await getUserData(session.userId);

  return (
    <div>
      <h1>Witaj, {userData.name}</h1>
      <RealTimeAnalytics userId={session.userId} />
    </div>
  );
}

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)

// app/blog/[slug]/page.tsx
import { Metadata } from 'next';

export async function generateMetadata({ params }): Promise<Metadata> {
  const post = await getPost((await params).slug);

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      images: [post.coverImage],
    },
  };
}

export async function generateStaticParams() {
  const posts = await getAllPosts();

  return posts.map((post) => ({
    slug: post.slug,
  }));
}

async function getPost(slug: string) {
  const res = await fetch(`https://cms.quantum.com/posts/${slug}`, {
    cache: 'force-cache'
  });
  return res.json();
}

export default async function BlogPost({ params }) {
  const post = await getPost((await params).slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <time>{post.publishedAt}</time>
      <Image
        src={post.coverImage}
        alt={post.title}
        width={1200}
        height={630}
        preload
      />
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

Metryki:

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

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

// app/products/[id]/page.tsx

async function getProduct(id: string) {
  const res = await fetch(`https://api.quantum-shop.com/products/${id}`, {
    next: { revalidate: 600 } // Rewalidacja co 10 minut
  });

  if (!res.ok) {
    notFound();
  }

  return res.json();
}

export async function generateStaticParams() {
  // Generuj tylko top 100 produktów podczas build
  const topProducts = await fetch('https://api.quantum-shop.com/products/top?limit=100')
    .then(res => res.json());

  return topProducts.map((product) => ({
    id: product.id.toString(),
  }));
}

export default async function ProductPage({ params }) {
  const product = await getProduct((await params).id);

  return (
    <div className="product-page">
      <ProductGallery images={product.images} />

      <div className="product-info">
        <h1>{product.name}</h1>

        <div className="price-stock">
          <span className="price">{product.price} QC</span>
          <StockIndicator stock={product.stock} />
        </div>

        <div className="description">
          {product.description}
        </div>

        <AddToCartButton productId={product.id} />

        <ProductReviews productId={product.id} />
      </div>

      <RelatedProducts categoryId={product.categoryId} />
    </div>
  );
}

export 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)

// app/dashboard/analytics/page.tsx

export const dynamic = 'force-dynamic';
export const fetchCache = 'force-no-store';

async function getRealTimeAnalytics(userId: string) {
  const res = await fetch(`https://api.quantum.com/analytics/${userId}`, {
    cache: 'no-store',
    headers: {
      Authorization: `Bearer ${process.env.API_TOKEN}`
    }
  });

  return res.json();
}

export default async function AnalyticsDashboard() {
  const session = await getServerSession();

  if (!session) {
    redirect('/login');
  }

  const analytics = await getRealTimeAnalytics(session.user.id);

  return (
    <div className="analytics-dashboard">
      <h1>Analityka w czasie rzeczywistym</h1>

      <div className="stats-grid">
        <StatCard
          title="Aktywni użytkownicy"
          value={analytics.activeUsers}
          change={analytics.activeUsersChange}
        />

        <StatCard
          title="Przychody dzisiaj"
          value={`${analytics.todayRevenue} QC`}
          change={analytics.revenueChange}
        />

        <StatCard
          title="Konwersje"
          value={`${analytics.conversionRate}%`}
          change={analytics.conversionChange}
        />
      </div>

      <RevenueChart data={analytics.revenueChart} />
      <UserActivityFeed data={analytics.recentActivity} />
    </div>
  );
}

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)

// app/news/page.tsx

async function getLatestNews() {
  const res = await fetch('https://api.quantum-news.com/latest', {
    next: { revalidate: 60 } // Rewalidacja co minutę
  });

  return res.json();
}

export default async function NewsFeed() {
  const initialNews = await getLatestNews();

  return (
    <div className="news-feed">
      <h1>Wiadomości z Metropolii Quantum</h1>

      {/* Server-rendered initial content (ISR) */}
      <NewsList initialNews={initialNews} />

      {/* Client-side updates co 30 sekund */}
      <ClientNewsUpdater />
    </div>
  );
}

export const revalidate = 60;
// components/ClientNewsUpdater.tsx
'use client';

import { useEffect, useState } from 'react';

export function ClientNewsUpdater() {
  const [latestNews, setLatestNews] = useState([]);

  useEffect(() => {
    const interval = setInterval(async () => {
      const res = await fetch('/api/news/latest');
      const data = await res.json();
      setLatestNews(data);
    }, 30000); // 30 sekund

    return () => clearInterval(interval);
  }, []);

  if (latestNews.length === 0) return null;

  return (
    <div className="breaking-news">
      <h2>Najnowsze wiadomości</h2>
      {latestNews.map(news => (
        <NewsCard key={news.id} news={news} />
      ))}
    </div>
  );
}

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 16

// app/layout.tsx
import { SpeedInsights } from '@vercel/speed-insights/next';
import { Analytics } from '@vercel/analytics/react';

export default function RootLayout({ children }) {
  return (
    <html lang="pl">
      <body>
        {children}
        <SpeedInsights />
        <Analytics />
      </body>
    </html>
  );
}

2. Custom Web Vitals reporting

// app/web-vitals.tsx
'use client';

import { useReportWebVitals } from 'next/web-vitals';

export function WebVitals() {
  useReportWebVitals((metric) => {
    // Wysyłanie do własnego analytics
    const body = JSON.stringify({
      name: metric.name,
      value: metric.value,
      rating: metric.rating,
      delta: metric.delta,
      id: metric.id,
    });

    fetch('/api/analytics/web-vitals', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body,
    });
  });

  return null;
}

3. Lighthouse CI w pipeline

# .github/workflows/lighthouse.yml
name: Lighthouse CI

on:
  pull_request:
    branches: [main]

jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Run Lighthouse CI
        run: |
          npm install -g @lhci/cli
          lhci autorun

      - name: Upload results
        uses: actions/upload-artifact@v3
        with:
          name: lighthouse-results
          path: .lighthouseci

Optymalizacja dla każdej strategii

SSG Optimizations

// next.config.js
module.exports = {
  images: {
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200],
  },
  compress: true,
  poweredByHeader: false,
  generateEtags: true,
};

ISR Optimizations

// Użyj on-demand revalidation dla ważnych aktualizacji
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';
import { NextRequest } from 'next/server';

export async function POST(request: NextRequest) {
  const secret = request.nextUrl.searchParams.get('secret');

  if (secret !== process.env.REVALIDATION_TOKEN) {
    return Response.json({ message: 'Invalid token' }, { status: 401 });
  }

  const path = request.nextUrl.searchParams.get('path');

  if (path) {
    revalidatePath(path);
    return Response.json({ revalidated: true });
  }

  return Response.json({ message: 'Missing path' }, { status: 400 });
}

SSR Optimizations

// Użyj streaming dla szybszego TTFB
// app/dashboard/page.tsx
import { Suspense } from 'react';

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>

      <Suspense fallback={<AnalyticsSkeleton />}>
        <Analytics />
      </Suspense>

      <Suspense fallback={<ChartSkeleton />}>
        <RevenueChart />
      </Suspense>
    </div>
  );
}

Podsumowanie i Decision Tree

Czy dane muszą być w czasie rzeczywistym?
├─ TAK → SSR
│   └─ Rozważ streaming i Suspense dla lepszej wydajności
│
└─ NIE
    │
    ├─ Czy dane zmieniają się częściej niż raz na godzinę?
    │   ├─ TAK → ISR (revalidate: 60-3600)
    │   │   └─ Rozważ on-demand revalidation dla ważnych aktualizacji
    │   │
    │   └─ NIE → SSG
    │       └─ Użyj generateStaticParams dla wszystkich możliwych ścieżek
    │
    └─ Czy potrzebujesz personalizacji?
        ├─ TAK → SSR lub ISR + Client Components
        └─ 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!

Kod do tej lekcji: App.tsx
// CSS Modules - Tworzenie scoped styles w Next.js
import React from 'react';
import styles from './Card.module.css';

// Ten komponent demonstruje CSS Modules w Next.js
// Klasy CSS sa automatycznie unikalne (scoped)

export default function CSSModulesDemo() {
  return (
    <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#fff' }}>
      <h1 style={{ color: '#64ffda', marginBottom: '20px' }}>CSS Modules w Next.js</h1>

      <div className={styles.card}>
        <h2 className={styles.cardTitle}>Quantum Energy Monitor</h2>
        <p className={styles.cardDescription}>
          Monitorowanie poziomu energii w sektorze Alpha
        </p>
        <span className={styles.badge}>Active</span>
      </div>

      <div className={styles.card}>
        <h2 className={styles.cardTitle}>Transport Hub</h2>
        <p className={styles.cardDescription}>
          Status sieci transportowej Metropolis
        </p>
        <span className={styles.badge}>Online</span>
      </div>
    </div>
  );
}

Hasła z dokumentacji w tej lekcji

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Którą opcją fetch wymusza się statyczne generowanie (SSG) w Next.js 16 według lekcji?

  2. 2. Jak w lekcji konfigurowano ISR (przyrostowe odświeżanie statyczne)?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Edytor kodu

    Zbuduj kompletny design system dla aplikacji Next.js: 1. Konfiguracja - stwórz struktura folderów dla design systemu 2. Design tokens - zdefiniuj kolory, typography, spacings w CSS custom properties 3. Layout system - stwórz grid system z CSS Grid i Flexbox 4. Component library - zbuduj Button, Card, Modal komponenty z variants 5. Theme system - implementuj light/dark mode z CSS variables 6. Responsive system - stwórz breakpoints i utility classes 7. Animation system - dodaj micro-interactions i transitions 8. Accessibility - implementuj focus states i ARIA attributes Użyj wszystkich poznanych technik: CSS Modules dla komponentów, Tailwind dla utilities, CSS-in-JS dla dynamicznych stylów.

Przydatne artykuły