Kurs Next.js · Moduł 5: Strategie renderowania

Strategie renderowania w Next.js App Router

9 min czytania
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:

  1. Renderowanie na serwerze - komponenty są renderowane na serwerze, a do klienta trafia już gotowy HTML
  2. Brak wliczania do bundla JavaScript - kod RSC nie jest wysyłany do przeglądarki
  3. Bezpośredni dostęp do zasobów serwerowych - bazy danych, system plików itp.
  4. 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 funkcji fetch lub revalidate: 0, aby wskazać, że dane powinny być pobierane przy każdym żądaniu
  • Komponenty są asynchroniczne (async), co pozwala na bezpośrednie użycie await w 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ły fetch

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 fetch za pomocą opcji next: { 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, useEffect itp.)
  • 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 FilterSidebar jest Client Component, umożliwiający interaktywne filtrowanie po stronie klienta
  • Komponenty CategoryHeader i ProductList są Server Components renderowane statycznie

Kiedy używać której strategii?

Wybór strategii renderowania zależy od charakterystyki danej części aplikacji:

  1. Strony statyczne (dokumentacja, landing page, strony informacyjne):

    • Użyj SSG (domyślne zachowanie)
  2. Treści dynamiczne, ale nie wymagające najnowszych danych (blog, katalog produktów):

    • Użyj ISR z odpowiednim czasem rewalidacji
  3. Treści wymagające najnowszych danych (ceny akcji, wiadomości, wyniki sportowe):

    • Użyj SSR (cache: 'no-store')
  4. Treści spersonalizowane i interaktywne elementy interfejsu (panele kontrolne, formularze):

    • Użyj CSR ('use client')
  5. 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 = false w 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:

  1. 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}
  1. 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

CechaSSGISRSSRCSR
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. 1. Co oznacza Server-Side Rendering (SSR) w kontekście Next.js?

Przydatne artykuły