Kurs Next.js · Moduł 5: Strategie renderowania
Strategie renderowania w Next.js App Router
W tej lekcji9
Next.js zrewolucjonizował sposób tworzenia aplikacji React, oferując różne strategie renderowania, które można wybierać zależnie od charakterystyki strony i wymagań projektu. W tym module omówimy, jak strategie renderowania działają w kontekście App Router, wprowadzonego w Next.js 13 i rozwijanego w kolejnych wersjach.
Ewolucja renderowania w Next.js
Next.js przeszedł istotną ewolucję w podejściu do renderowania. W wersjach przed 13, używaliśmy specjalnych funkcji jak getStaticProps, getServerSideProps czy getStaticPaths, aby określić strategię renderowania dla danej strony. Każda strona musiała wybrać jedną konkretną strategię.
W App Router podejście jest inne - renderowanie jest teraz bardziej granularne i opiera się na koncepcji React Server Components. Zamiast określać strategię dla całej strony, możemy mieszać różne podejścia na poziomie poszczególnych komponentów.
React Server Components - fundament App Router
U podstaw nowego modelu renderowania leżą React Server Components (RSC), które zmieniły sposób myślenia o aplikacjach React. Główne cechy RSC to:
- Renderowanie na serwerze - komponenty są renderowane na serwerze, a do klienta trafia już gotowy HTML
- Brak wliczania do bundla JavaScript - kod RSC nie jest wysyłany do przeglądarki
- Bezpośredni dostęp do zasobów serwerowych - bazy danych, system plików itp.
- Brak dostępu do API przeglądarki - nie można używać hooks ani obsługiwać zdarzeń
Strategie renderowania w App Router
W App Router wciąż możemy korzystać z tradycyjnych strategii renderowania, ale sposób ich implementacji jest inny. Przyjrzyjmy się każdej z nich.
1. Renderowanie po stronie serwera (SSR - Server-Side Rendering)
W App Router, SSR jest domyślnym zachowaniem dla Route Handlers i Server Components.
1// app/products/page.tsx
2import { Product } from '@/components/Product';
3
4async function getData() {
5 // Ta funkcja uruchamia się dla każdego żądania na serwerze
6 const res = await fetch('https://api.example.com/products', { cache: 'no-store' });
7
8 if (!res.ok) {
9 throw new Error('Failed to fetch data');
10 }
11
12 return res.json();
13}
14
15export default async function ProductsPage() {
16 const products = await getData();
17
18 return (
19 <div>
20 <h1>Nasze produkty</h1>
21 <div className="grid">
22 {products.map(product => (
23 <Product key={product.id} data={product} />
24 ))}
25 </div>
26 </div>
27 );
28}Kluczowe cechy SSR w App Router:
- Używamy opcji
cache: 'no-store'w funkcjifetchlubrevalidate: 0, aby wskazać, że dane powinny być pobierane przy każdym żądaniu - Komponenty są asynchroniczne (
async), co pozwala na bezpośrednie użycieawaitw ciele komponentu - Każde żądanie do strony powoduje ponowne renderowanie strony na serwerze
Zalety SSR:
- Zawsze aktualne dane
- Lepsza wydajność SEO (wyszukiwarki widzą pełną treść strony)
- Możliwość personalizacji treści dla każdego użytkownika
Wady SSR:
- Wolniejsze ładowanie strony (serwer musi przetworzyć każde żądanie)
- Większe obciążenie serwera
2. Generowanie statyczne (SSG - Static Site Generation)
W App Router, SSG osiągamy poprzez domyślne zachowanie fetch bez opcji cache: 'no-store'.
1// app/blog/page.tsx
2import { BlogPost } from '@/components/BlogPost';
3
4async function getData() {
5 // Ta funkcja uruchamia się tylko w czasie budowania (lub na żądanie w trybie dev)
6 const res = await fetch('https://api.example.com/blog-posts');
7
8 if (!res.ok) {
9 throw new Error('Failed to fetch data');
10 }
11
12 return res.json();
13}
14
15export default async function BlogPage() {
16 const posts = await getData();
17
18 return (
19 <div>
20 <h1>Nasz blog</h1>
21 <div className="grid">
22 {posts.map(post => (
23 <BlogPost key={post.id} data={post} />
24 ))}
25 </div>
26 </div>
27 );
28}Kluczowe cechy SSG w App Router:
- Domyślnie wszystkie komponenty są renderowane statycznie, jeśli nie określono inaczej
- Dane są pobierane w czasie budowania aplikacji i "zamrażane" w statycznym HTML
- Nie ma potrzeby używania specjalnej funkcji jak
getStaticProps- wystarczy zwykłyfetch
Zalety SSG:
- Bardzo szybkie ładowanie stron (serwowane są statyczne pliki HTML)
- Minimalne obciążenie serwera
- Dobra wydajność SEO
Wady SSG:
- Dane mogą być nieaktualne (aktualizowane tylko przy przebudowie)
- Nie nadaje się do stron z treścią spersonalizowaną dla użytkownika
3. Przyrostowa regeneracja statyczna (ISR - Incremental Static Regeneration)
W App Router, ISR osiągamy poprzez dodanie opcji revalidate do funkcji fetch lub do segmentu trasy.
1// app/products/[id]/page.tsx
2import { ProductDetails } from '@/components/ProductDetails';
3import { notFound } from 'next/navigation';
4
5// Opcja 1: Rewalidacja na poziomie strony
6export const revalidate = 3600; // Rewalidacja co godzinę
7
8async function getProduct(id) {
9 // Opcja 2: Rewalidacja na poziomie funkcji fetch
10 const res = await fetch(`https://api.example.com/products/${id}`, {
11 next: { revalidate: 3600 } // Również co godzinę
12 });
13
14 if (!res.ok) {
15 return null;
16 }
17
18 return res.json();
19}
20
21export default async function ProductPage({ params }) {
22 const product = await getProduct((await params).id);
23
24 if (!product) {
25 notFound();
26 }
27
28 return <ProductDetails product={product} />;
29}Kluczowe cechy ISR w App Router:
- Możemy określić czas rewalidacji na poziomie komponentu za pomocą eksportowanej zmiennej
revalidate - Możemy określić czas rewalidacji na poziomie żądania
fetchza pomocą opcjinext: { revalidate: seconds } - Istnieje możliwość wymuszenia rewalidacji za pomocą API Route Handler
Zalety ISR:
- Łączy zalety SSG (szybkość) i SSR (względna aktualność danych)
- Pozwala na aktualizację treści bez przebudowy całej aplikacji
- Dobre wsparcie dla SEO
Wady ISR:
- Dane nie są w 100% aktualne (istnieje opóźnienie zależne od ustawionego czasu rewalidacji)
- Złożoniejsza implementacja i debugowanie
4. Renderowanie po stronie klienta (CSR - Client-Side Rendering)
W App Router, CSR osiągamy poprzez oznaczenie komponentu jako "Client Component" przy użyciu dyrektywy 'use client' na początku pliku.
1'use client';
2
3// app/dashboard/page.tsx
4import { useState, useEffect } from 'react';
5
6export default function Dashboard() {
7 const [data, setData] = useState(null);
8 const [isLoading, setIsLoading] = useState(true);
9
10 useEffect(() => {
11 async function fetchData() {
12 try {
13 const res = await fetch('/api/dashboard-data');
14 const jsonData = await res.json();
15 setData(jsonData);
16 } catch (error) {
17 console.error('Error fetching data:', error);
18 } finally {
19 setIsLoading(false);
20 }
21 }
22
23 fetchData();
24 }, []);
25
26 if (isLoading) return <div>Ładowanie danych...</div>;
27
28 return (
29 <div>
30 <h1>Panel kontrolny</h1>
31 {/* Renderowanie danych pobranych po stronie klienta */}
32 {data && data.map(item => (
33 <div key={item.id}>{item.name}</div>
34 ))}
35 </div>
36 );
37}Kluczowe cechy CSR w App Router:
- Komponent musi być oznaczony dyrektywą
'use client' - Możemy używać hooków React (
useState,useEffectitp.) - Dane są pobierane po załadowaniu strony w przeglądarce
Zalety CSR:
- Może oferować lepsze doświadczenie użytkownika dla aplikacji interaktywnych
- Mniejsze obciążenie serwera (dane są pobierane bezpośrednio z API w przeglądarce)
- Dobra opcja dla treści spersonalizowanych dla zalogowanego użytkownika
Wady CSR:
- Słabsza wydajność SEO (wyszukiwarki mogą nie widzieć wszystkich treści)
- Wolniejsze początkowe ładowanie treści (użytkownik widzi stan ładowania)
- Większy rozmiar plików JavaScript przesyłanych do przeglądarki
Mieszane podejście - najlepsza praktyka w App Router
Jedną z największych zalet App Router jest możliwość mieszania różnych strategii renderowania w ramach jednej aplikacji, a nawet jednej strony. Dzięki temu możemy wybrać optymalną strategię dla każdego komponentu.
Przykład wykorzystania mieszanych strategii:
1// app/products/[category]/page.tsx
2import { ProductList } from '@/components/ProductList';
3import { CategoryHeader } from '@/components/CategoryHeader';
4import FilterSidebar from '@/components/FilterSidebar'; // Client Component
5
6// Ta strona i jej dane są renderowane statycznie
7// z rewalidacją co godzinę
8export const revalidate = 3600;
9
10// Określamy, które kategorie mają być generowane statycznie
11export async function generateStaticParams() {
12 const categories = await fetch('https://api.example.com/categories').then(res => res.json());
13
14 return categories.map(category => ({
15 category: category.slug,
16 }));
17}
18
19async function getCategoryData(category) {
20 const categoryData = await fetch(`https://api.example.com/categories/${category}`);
21 return categoryData.json();
22}
23
24async function getProducts(category) {
25 const products = await fetch(`https://api.example.com/products?category=${category}`);
26 return products.json();
27}
28
29export default async function CategoryPage({ params }) {
30 // Pobieramy dane równolegle
31 const [categoryData, products] = await Promise.all([
32 getCategoryData((await params).category),
33 getProducts((await params).category),
34 ]);
35
36 return (
37 <div className="grid grid-cols-4 gap-4">
38 {/* Statycznie renderowany nagłówek kategorii */}
39 <CategoryHeader data={categoryData} />
40
41 {/* Interaktywny filtr renderowany po stronie klienta */}
42 <div className="col-span-1">
43 <FilterSidebar initialProducts={products} />
44 </div>
45
46 {/* Statycznie renderowana lista produktów */}
47 <div className="col-span-3">
48 <ProductList products={products} />
49 </div>
50 </div>
51 );
52}W tym przykładzie:
- Cała strona jest renderowana statycznie z rewalidacją co godzinę (ISR)
- Tylko określone kategorie są generowane w czasie budowania (
generateStaticParams) - Komponent
FilterSidebarjest Client Component, umożliwiający interaktywne filtrowanie po stronie klienta - Komponenty
CategoryHeaderiProductListsą Server Components renderowane statycznie
Kiedy używać której strategii?
Wybór strategii renderowania zależy od charakterystyki danej części aplikacji:
Strony statyczne (dokumentacja, landing page, strony informacyjne):
- Użyj SSG (domyślne zachowanie)
Treści dynamiczne, ale nie wymagające najnowszych danych (blog, katalog produktów):
- Użyj ISR z odpowiednim czasem rewalidacji
Treści wymagające najnowszych danych (ceny akcji, wiadomości, wyniki sportowe):
- Użyj SSR (
cache: 'no-store')
- Użyj SSR (
Treści spersonalizowane i interaktywne elementy interfejsu (panele kontrolne, formularze):
- Użyj CSR (
'use client')
- Użyj CSR (
Hybrydowe podejście (np. e-commerce):
- Statyczny szkielet strony (SSG/ISR)
- Dynamiczne dane produktów (SSR)
- Interaktywne elementy jak koszyk, filtry (CSR)
Dynamiczne i statyczne trasy
W App Router, możemy również określić, które ścieżki dynamiczne powinny być generowane statycznie podczas budowania aplikacji:
1// app/blog/[slug]/page.tsx
2export async function generateStaticParams() {
3 const posts = await fetch('https://api.example.com/posts').then(res => res.json());
4
5 return posts.map((post) => ({
6 slug: post.slug,
7 }));
8}Dla tras, które nie są objęte przez generateStaticParams:
- Jeśli ustawiliśmy
dynamicParams = falsew opcjach segmentu, Next.js zwróci 404 - Jeśli
dynamicParams = true(domyślnie) lub nie jest ustawione, Next.js wygeneruje stronę na żądanie (SSR)
Optymalizacja stron dynamicznych
Jeśli mamy wiele dynamicznych podstron (np. tysiące produktów w e-commerce), możemy zoptymalizować proces budowania poprzez:
- Generowanie najpopularniejszych stron statycznie podczas budowania:
1export async function generateStaticParams() {
2 // Pobierz tylko najpopularniejsze produkty
3 const popularProducts = await fetch('https://api.example.com/products/popular').then(res => res.json());
4
5 return popularProducts.map((product) => ({
6 id: product.id,
7 }));
8}- On-demand Revalidation - rewalidacja poszczególnych stron na żądanie, np. po aktualizacji produktu:
1// app/api/revalidate/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { revalidatePath } from 'next/cache';
4
5export async function POST(request: NextRequest) {
6 const { path, secret } = await request.json();
7
8 // Sprawdzenie tajnego klucza dla bezpieczeństwa
9 if (secret !== process.env.REVALIDATION_SECRET) {
10 return NextResponse.json({ message: 'Invalid secret' }, { status: 401 });
11 }
12
13 if (!path) {
14 return NextResponse.json({ message: 'Path is required' }, { status: 400 });
15 }
16
17 try {
18 // Rewalidacja określonej ścieżki
19 revalidatePath(path);
20 return NextResponse.json({ revalidated: true, message: `Revalidated ${path}` });
21 } catch (err) {
22 return NextResponse.json({ message: 'Error revalidating' }, { status: 500 });
23 }
24}Porównanie strategii renderowania
| Cecha | SSG | ISR | SSR | CSR |
|---|---|---|---|---|
| Wydajność | ||||
| SEO | ||||
| Aktualność danych | ||||
| Obciążenie serwera | ||||
| Interaktywność |
Podsumowanie
App Router w Next.js oferuje zaawansowane możliwości mieszania różnych strategii renderowania na poziomie komponentów, co pozwala na tworzenie wydajnych i elastycznych aplikacji. Kluczem do sukcesu jest wybór odpowiedniej strategii dla każdej części aplikacji, biorąc pod uwagę wymagania dotyczące wydajności, SEO i aktualności danych.
W następnym module omówimy bardziej szczegółowo mechanizmy rewalidacji danych w Next.js App Router, które są kluczowe dla implementacji ISR i zapewnienia aktualności treści.
Kod do tej lekcji: App.tsx
1// Demo: Strategie renderowania w Next.js App Router
2// SSG (Static), SSR (Dynamic), ISR (Incremental Static Regeneration)
3import React, { useState } from 'react';
4
5// Next.js App Router domyślnie używa Static Rendering (SSG)
6// Dynamiczne renderowanie włącza się automatycznie gdy:
7// - Użyjesz cookies(), headers(), searchParams
8// - fetch() z { cache: 'no-store' }
9// - export const dynamic = 'force-dynamic'
10
11interface RenderingStrategy {
12 name: string;
13 shortName: string;
14 color: string;
15 when: string;
16 code: string;
17 pros: string[];
18 cons: string[];
19 buildTime: boolean;
20 requestTime: boolean;
21}
22
23const strategies: RenderingStrategy[] = [
24 {
25 name: 'Static Site Generation',
26 shortName: 'SSG',
27 color: '#7c4dff',
28 when: 'Build time (domyślnie w App Router)',
29 code: "// Domyślne zachowanie - nie trzeba nic dodawać\nexport default async function Page() {\n const data = await fetch(url); // cached\n return <div>{data}</div>;\n}",
30 pros: ['Najszybsze - HTML gotowy z CDN', 'Najtańsze - zero obliczeń per request', 'SEO-friendly'],
31 cons: ['Dane statyczne do następnego buildu', 'Nie nadaje się do personalizacji'],
32 buildTime: true,
33 requestTime: false,
34 },
35 {
36 name: 'Server-Side Rendering',
37 shortName: 'SSR',
38 color: '#64ffda',
39 when: 'Każde żądanie (on-demand)',
40 code: "// Wymuś dynamiczne renderowanie\nexport const dynamic = 'force-dynamic';\n// lub użyj cookies/headers\nimport { cookies } from 'next/headers';\n\nexport default async function Page() {\n const session = cookies().get('token');\n const data = await fetch(url, {\n cache: 'no-store'\n });\n return <div>{data}</div>;\n}",
41 pros: ['Zawsze aktualne dane', 'Personalizacja per-user', 'SEO-friendly'],
42 cons: ['Wolniejsze - serwer przetwarza każdy request', 'Wyższe koszty serwera'],
43 buildTime: false,
44 requestTime: true,
45 },
46 {
47 name: 'Incremental Static Regeneration',
48 shortName: 'ISR',
49 color: '#ff9800',
50 when: 'Build + odświeżanie co N sekund',
51 code: "// Rewalidacja co 60 sekund\nexport const revalidate = 60;\n\nexport default async function Page() {\n const data = await fetch(url, {\n next: { revalidate: 60 }\n });\n return <div>{data}</div>;\n}\n\n// Lub on-demand:\nimport { revalidatePath } from 'next/cache';\nrevalidatePath('/products');",
52 pros: ['Balans: szybkość SSG + aktualność SSR', 'Niskie koszty', 'Skalowalne'],
53 cons: ['Dane mogą być nieaktualne przez N sekund', 'Bardziej złożona konfiguracja'],
54 buildTime: true,
55 requestTime: true,
56 },
57];
58
59function StrategyCard({ strategy, isActive, onClick }: {
60 strategy: RenderingStrategy; isActive: boolean; onClick: () => void;
61}) {
62 return (
63 <div onClick={onClick} style={{
64 background: isActive ? `${strategy.color}12` : 'rgba(255,255,255,0.02)',
65 border: `2px solid ${isActive ? strategy.color : 'rgba(255,255,255,0.08)'}`,
66 borderRadius: '12px', padding: '20px', cursor: 'pointer',
67 transition: 'all 0.3s ease',
68 }}>
69 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
70 <h3 style={{ color: strategy.color, margin: 0 }}>{strategy.shortName}</h3>
71 <div style={{ display: 'flex', gap: '6px' }}>
72 {strategy.buildTime && (
73 <span style={{ background: '#7c4dff20', color: '#7c4dff', padding: '2px 8px', borderRadius: '10px', fontSize: '0.7rem' }}>
74 Build
75 </span>
76 )}
77 {strategy.requestTime && (
78 <span style={{ background: '#64ffda20', color: '#64ffda', padding: '2px 8px', borderRadius: '10px', fontSize: '0.7rem' }}>
79 Request
80 </span>
81 )}
82 </div>
83 </div>
84 <p style={{ color: '#b0bec5', fontSize: '0.85rem', margin: '0 0 8px' }}>{strategy.name}</p>
85 <p style={{ color: '#78909c', fontSize: '0.8rem', margin: 0 }}>{strategy.when}</p>
86 </div>
87 );
88}
89
90export default function RenderingStrategies() {
91 const [active, setActive] = useState(0);
92 const s = strategies[active];
93
94 return (
95 <div style={{
96 background: '#0f0f23', minHeight: '100vh', padding: '24px',
97 color: '#fff', fontFamily: 'system-ui, sans-serif',
98 }}>
99 <h1 style={{ color: '#64ffda', marginBottom: '8px' }}>Strategie Renderowania</h1>
100 <p style={{ color: '#b0bec5', marginBottom: '24px' }}>
101 Next.js App Router - SSG vs SSR vs ISR
102 </p>
103
104 <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '12px', marginBottom: '24px' }}>
105 {strategies.map((st, i) => (
106 <StrategyCard key={st.shortName} strategy={st} isActive={i === active} onClick={() => setActive(i)} />
107 ))}
108 </div>
109
110 <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
111 <div style={{
112 background: 'rgba(0,0,0,0.3)', borderRadius: '12px', padding: '20px',
113 }}>
114 <h4 style={{ color: s.color, marginBottom: '12px' }}>Kod</h4>
115 <pre style={{ margin: 0, color: '#b0bec5', fontSize: '0.8rem', lineHeight: 1.6, whiteSpace: 'pre-wrap' }}>
116 {s.code}
117 </pre>
118 </div>
119 <div style={{ display: 'grid', gap: '16px' }}>
120 <div style={{ background: 'rgba(76,175,80,0.08)', borderRadius: '12px', padding: '16px' }}>
121 <h4 style={{ color: '#4caf50', marginBottom: '8px' }}>Zalety</h4>
122 {s.pros.map(p => (
123 <p key={p} style={{ color: '#b0bec5', fontSize: '0.85rem', margin: '4px 0' }}>+ {p}</p>
124 ))}
125 </div>
126 <div style={{ background: 'rgba(244,67,54,0.08)', borderRadius: '12px', padding: '16px' }}>
127 <h4 style={{ color: '#f44336', marginBottom: '8px' }}>Wady</h4>
128 {s.cons.map(c => (
129 <p key={c} style={{ color: '#b0bec5', fontSize: '0.85rem', margin: '4px 0' }}>- {c}</p>
130 ))}
131 </div>
132 </div>
133 </div>
134 </div>
135 );
136}Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Co oznacza Server-Side Rendering (SSR) w kontekście Next.js?