Kurs Next.js · Moduł 3: Stylowanie i optymalizacja wizualna
Performance Benchmarks w Next.js 16 - SSR vs SSG vs ISR
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 rewalidacjiZalety:
- 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 |
// 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:
| 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
// 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:
| 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
// 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 minutMetryki:
- 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: .lighthouseciOptymalizacja 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 ISRKluczowe wnioski:
- SSG - najszybszy, ale statyczny (blogi, marketing, dokumentacja)
- ISR - balance (e-commerce, news, public profiles)
- SSR - zawsze świeży (dashboards, search, user content)
- 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. Którą opcją fetch wymusza się statyczne generowanie (SSG) w Next.js 16 według lekcji?
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.