Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Strategie renderowania w Next.js App Router

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

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

Ir a CodeWorlds