Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Częściowe prerenderowanie (Partial Prerendering)

W poprzednich modułach omawialiśmy różne strategie renderowania w Next.js, w tym streamowanie danych i implementację stanów ładowania. W tym module skupimy się na jednej z najnowszych i najbardziej ekscytujących funkcji Next.js - Częściowym Prerenderowaniu (Partial Prerendering, PPR), które łączy zalety statycznego generowania i dynamicznego renderowania w jednym hybrydowym podejściu.

Czym jest Częściowe Prerenderowanie?

Częściowe Prerenderowanie (Partial Prerendering) to wzorzec renderowania wprowadzony w Next.js 14, który pozwala na statyczne generowanie części strony podczas budowania, podczas gdy inne części mogą być dynamicznie renderowane podczas żądania.

Najprościej mówiąc, PPR pozwala na:

  1. Natychmiastowe renderowanie statycznych części strony (np. layout, szablon, nagłówki)
  2. Opóźnione renderowanie dynamicznych części, które wymagają świeżych danych

Taka hybrydowa architektura łączy zalety obu światów: szybkość statycznego generowania z aktualnością dynamicznego renderowania.

Problemy, które rozwiązuje Częściowe Prerenderowanie

Przed wprowadzeniem PPR, deweloperzy musieli wybierać między kilkoma podejściami, każde z własnymi ograniczeniami:

  1. Static Site Generation (SSG) - szybka dostawa treści, ale problemy z aktualnością danych
  2. Server-Side Rendering (SSR) - zawsze aktualne dane, ale wolniejsze TTFB (Time To First Byte)
  3. Incremental Static Regeneration (ISR) - kompromis między SSG i SSR, ale z ograniczeniami czasowymi
  4. Client-Side Rendering (CSR) - interaktywność kosztem wydajności SEO

PPR stara się rozwiązać te problemy, wprowadzając nowy model hybrydowy, w którym podejścia te mogą współistnieć na jednej stronie w sposób bardziej zintegrowany.

Jak działa Częściowe Prerenderowanie?

PPR wprowadza nową jednostkę renderowania - "hole" (dziury) lub "slots" (gniazda), które są umieszczane w statycznie wygenerowanej treści. Te dziury są później wypełniane dynamicznie renderowanymi treściami po stronie klienta.

Process ten można podzielić na cztery etapy:

  1. Statyczne wstępne renderowanie - podczas budowania, Next.js generuje statyczną Shell HTML z "dziurami" dla dynamicznych części
  2. Natychmiastowa odpowiedź - gdy użytkownik żąda strony, serwer natychmiast odpowiada prerenerowaną treścią Shell
  3. Dynamiczne wypełnianie - "dziury" są asynchronicznie wypełniane dynamicznymi danymi na serwerze
  4. Streaming treści - dynamiczne treści są streamowane do przeglądarki w miarę ich renderowania

Implementacja Częściowego Prerenderowania w Next.js

1. Konfiguracja PPR w Next.js

W Next.js 14 i nowszych, PPR można włączyć poprzez konfigurację w pliku

next.config.js
:

1// next.config.js
2module.exports = {
3  experimental: {
4    ppr: true
5  }
6}

Alternatywnie, możesz włączyć PPR dla konkretnych stron za pomocą dyrektywy w komponencie strony:

1export const runtime = 'nodejs';
2export const preferredRegion = 'auto';
3export const dynamic = 'force-dynamic';
4export const dynamicParams = true;
5
6export const generateStaticParams = async () => {
7  //
8}

2. Podstawowy przykład PPR

Oto przykład implementacji PPR w Next.js 15:

1// app/products/[category]/page.tsx
2import { Suspense } from 'react';
3import { CategoryHeader } from '@/components/category-header';
4import { ProductGrid } from '@/components/product-grid';
5import { ProductFilters } from '@/components/product-filters';
6import { LoadingSkeleton } from '@/components/loading-skeleton';
7
8// Statyczne parametry dla prerendrowanych stron
9export async function generateStaticParams() {
10  const categories = await fetchTopCategories();
11  return categories.map(category => ({ category: category.slug }));
12}
13
14// Statyczne dane, które są znane podczas budowania
15async function getCategoryData(categorySlug: string) {
16  // Pobierz podstawowe dane kategorii, które rzadko się zmieniają
17  const data = await fetch(`https://api.example.com/categories/${categorySlug}`, {
18    // Te dane będą pobrane podczas budowania aplikacji
19    // i będą częścią statycznego Shella
20  });
21  
22  return data.json();
23}
24
25export default async function CategoryPage({ params }: { params: Promise<{ category: string }> }) {
26  // Pobierz statyczne dane kategorii (renderowane podczas budowania)
27  const categoryData = await getCategoryData((await params).category);
28  
29  return (
30    <div className="container mx-auto py-8">
31      {/* Statycznie renderowany nagłówek - część Shella */}
32      <CategoryHeader category={categoryData} />
33      
34      <div className="grid grid-cols-4 gap-6 mt-8">
35        <div className="col-span-1">
36          {/* Dynamicznie renderowane filtry z danymi zależnymi od sesji użytkownika */}
37          <Suspense fallback={<LoadingSkeleton type="filters" />}>
38            <ProductFilters categorySlug={(await params).category} />
39          </Suspense>
40        </div>
41        
42        <div className="col-span-3">
43          {/* Dynamicznie renderowana lista produktów ze świeżymi danymi */}
44          <Suspense fallback={<LoadingSkeleton type="products" />}>
45            <ProductGrid categorySlug={(await params).category} />
46          </Suspense>
47        </div>
48      </div>
49    </div>
50  );
51}

W komponencie

ProductFilters
i
ProductGrid
, będziemy używać opcji
cache: 'no-store'
aby zapewnić, że dane będą zawsze świeże:

1// components/product-grid.tsx
2async function getProducts(categorySlug: string, filterParams?: Record<string, string>) {
3  // Tworzymy URL z parametrami filtrów
4  const queryParams = new URLSearchParams(filterParams);
5  const url = `https://api.example.com/products?category=${categorySlug}&${queryParams}`;
6  
7  // Używamy opcji no-store, aby zawsze pobierać świeże dane
8  const res = await fetch(url, { cache: 'no-store' });
9  
10  if (!res.ok) {
11    throw new Error('Failed to fetch products');
12  }
13  
14  return res.json();
15}
16
17export async function ProductGrid({ categorySlug }: { categorySlug: string }) {
18  // Pobierz aktualne parametry filtrowania z URL
19  const searchParams = useSearchParams();
20  const filterParams = Object.fromEntries(searchParams.entries());
21  
22  // Pobierz produkty z uwzględnieniem filtrów
23  const products = await getProducts(categorySlug, filterParams);
24  
25  return (
26    <div className="grid grid-cols-3 gap-4">
27      {products.map(product => (
28        <ProductCard key={product.id} product={product} />
29      ))}
30    </div>
31  );
32}

Zaawansowane wzorce PPR

1. Unikanie wodospadu danych z preloaderem

Jeden z problemów asynchronicznego renderowania to "wodospad danych", który możemy zminimalizować za pomocą wstępnego ładowania:

1// app/products/[category]/page.tsx
2import { Suspense } from 'react';
3import { ProductGrid } from './product-grid';
4
5// Funkcja preloadera inicjująca ładowanie danych
6function preloadProducts(categorySlug: string) {
7  // Inicjuje pobieranie danych, ale nie czeka na wynik
8  void getProducts(categorySlug);
9}
10
11export default async function CategoryPage({ params }: { params: Promise<{ category: string }> }) {
12  // Rozpocznij ładowanie danych wcześniej
13  preloadProducts((await params).category);
14  
15  return (
16    <div>
17      {/* Statycznie prerendrowany nagłówek */}
18      <h1>Kategoria: {(await params).category}</h1>
19      
20      {/* Dynamiczna część */}
21      <Suspense fallback={<div>Wczytywanie produktów...</div>}>  
22        <ProductGrid categorySlug={(await params).category} />
23      </Suspense>
24    </div>
25  );
26}
27
28// Oto jak wygląda funkcja getProducts w tym przypadku
29async function getProducts(categorySlug: string) {
30  // Funkcja z cache'owaniem wyników
31  // Można użyć biblioteki SWR lub React Query dla bardziej zaawansowanego cache'owania
32  const cacheKey = `products-${categorySlug}`;
33  if (!cache.has(cacheKey)) {
34    const promise = fetch(`https://api.example.com/products?category=${categorySlug}`, { 
35      cache: 'no-store' 
36    }).then(res => res.json());
37    
38    cache.set(cacheKey, promise);
39  }
40  
41  return cache.get(cacheKey);
42}

2. Ustawianie priorytetu ładowania sekcji

Możemy ustawić priorytet ładowania różnych części strony za pomocą zagłêbionego komponenta Suspense:

1// app/dashboard/page.tsx
2import { Suspense } from 'react';
3import { Header } from '@/components/header';
4import { UserWidget } from '@/components/user-widget';
5import { RecentActivity } from '@/components/recent-activity';
6import { PerformanceMetrics } from '@/components/performance-metrics';
7import { Recommendations } from '@/components/recommendations';
8
9import { 
10  UserWidgetSkeleton,
11  RecentActivitySkeleton,
12  PerformanceMetricsSkeleton,
13  RecommendationsSkeleton
14} from '@/components/skeletons';
15
16export default function DashboardPage() {
17  return (
18    <div className="container mx-auto py-8">
19      {/* Statyczna część - nagłówek */}
20      <Header title="Dashboard" />
21      
22      <div className="grid grid-cols-12 gap-6 mt-8">
23        {/* Priorytet 1: Widget użytkownika - ładowany pierwszy */}
24        <div className="col-span-3">
25          <Suspense fallback={<UserWidgetSkeleton />}>
26            <UserWidget />
27          </Suspense>
28        </div>
29        
30        <div className="col-span-9">
31          {/* Priorytet 2: Ostatnia aktywność - ładowana jako druga */}
32          <Suspense fallback={<RecentActivitySkeleton />}>
33            <RecentActivity />
34            
35            <div className="grid grid-cols-2 gap-6 mt-6">
36              {/* Priorytet 3: Metryki wydajności - ładowane jako trzecie */}
37              <Suspense fallback={<PerformanceMetricsSkeleton />}>
38                <PerformanceMetrics />
39              </Suspense>
40              
41              {/* Priorytet 4: Rekomendacje - ładowane jako ostatnie */}
42              <Suspense fallback={<RecommendationsSkeleton />}>
43                <Recommendations />
44              </Suspense>
45            </div>
46          </Suspense>
47        </div>
48      </div>
49    </div>
50  );
51}

3. Dynamiczne parametry z Częściowym Prerenderowaniem

Możemy połączyć statyczne generowanie i dynamiczne parametry:

1// app/products/[id]/page.tsx
2import { Suspense } from 'react';
3
4// Generujemy statycznie tylko określone strony produktów
5export async function generateStaticParams() {
6  // Pobieramy tylko popularne produkty do prerenderowania
7  const popularProducts = await fetchPopularProducts();
8  
9  return popularProducts.map(product => ({
10    id: product.id.toString()
11  }));
12}
13
14// Po włączeniu dynamicParams: true, produkty bez statycznego generowania
15// będą renderowane na żądanie
16export const dynamicParams = true;
17
18export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
19  // Ta funkcja pobiera podstawowe informacje o produkcie
20  // Dla popularnych produktów te dane zostaną pobrane podczas budowania
21  // Dla pozostałych - podczas żądania
22  const product = await getProductBasicInfo((await params).id);
23  
24  if (!product) {
25    return <div>Produkt nie znaleziony</div>;
26  }
27  
28  return (
29    <div className="product-page">
30      <h1>{product.name}</h1>
31      <div className="product-image">
32        <img src={product.imageUrl} alt={product.name} />
33      </div>
34      
35      <div className="product-info">
36        <p className="product-price">{product.price}</p>
37        
38        {/* Dynamiczna część z zawsze aktualnymi informacjami */}
39        <Suspense fallback={<div>Sprawdzanie dostępności...</div>}>
40          <ProductAvailability productId={(await params).id} />
41        </Suspense>
42        
43        <Suspense fallback={<div>Ładowanie opcji dostawy...</div>}>
44          <DeliveryOptions productId={(await params).id} />
45        </Suspense>
46      </div>
47      
48      <div className="product-details">
49        {/* Statyczna część - opis produktu */}
50        <div dangerouslySetInnerHTML={{ __html: product.description }} />
51        
52        {/* Dynamiczna część - recenzje, często aktualizowane */}
53        <Suspense fallback={<div>Ładowanie recenzji...</div>}>
54          <ProductReviews productId={(await params).id} />
55        </Suspense>
56      </div>
57    </div>
58  );
59}
60
61async function getProductBasicInfo(id: string) {
62  // Pobieranie podstawowych informacji o produkcie
63  // Może być cachowane, ponieważ te informacje rzadko się zmieniają
64  const res = await fetch(`https://api.example.com/products/${id}/basic`);
65  if (!res.ok) return null;
66  return res.json();
67}

4. Dynamiczny routing z Częściowym Prerenderowaniem

Możemy również użyć PPR z dynamicznymi routami, np. dla zaawansowanego filtrowania:

1// app/products/[[...slug]]/page.tsx
2import { Suspense } from 'react';
3
4// Generujemy popularne kombinacje filtrów/kategorii dla prerenderowania
5export async function generateStaticParams() {
6  return [
7    { slug: [] },                          // /products
8    { slug: ['category', 'electronics'] }, // /products/category/electronics
9    { slug: ['deals'] },                   // /products/deals
10    // ... inne popularne kombinacje
11  ];
12}
13
14export const dynamicParams = true; // Pozwala na dynamiczne renderowanie innych kombinacji
15
16export default async function ProductsPage({ params }: { params: Promise<{ slug?: string[] }> }) {
17  // Parsowanie parametrów slug
18  const { category, filters } = parseSlug((await params).slug || []);
19  
20  // Pobranie statycznych metadanych
21  const pageMetadata = await getPageMetadata(category);
22  
23  return (
24    <div className="container mx-auto py-8">
25      {/* Statyczna część - nagłówek i metadane */}
26      <h1>{pageMetadata.title}</h1>
27      <p>{pageMetadata.description}</p>
28      
29      <div className="flex mt-8">
30        {/* Dynamiczna część - filtry */}
31        <div className="w-1/4">
32          <Suspense fallback={<div>Ładowanie filtrów...</div>}>
33            <ProductFilters category={category} activeFilters={filters} />
34          </Suspense>
35        </div>
36        
37        {/* Dynamiczna część - lista produktów */}
38        <div className="w-3/4">
39          <Suspense fallback={<div>Ładowanie produktów...</div>}>
40            <ProductList category={category} filters={filters} />
41          </Suspense>
42        </div>
43      </div>
44    </div>
45  );
46}
47
48// Funkcja pomocnicza do parsowania parametrów slug
49function parseSlug(slug: string[]) {
50  let category = '';
51  const filters: Record<string, string> = {};
52  
53  for (let i = 0; i < slug.length; i += 2) {
54    if (slug[i] === 'category' && slug[i+1]) {
55      category = slug[i+1];
56    } else if (slug[i] && slug[i+1]) {
57      filters[slug[i]] = slug[i+1];
58    }
59  }
60  
61  return { category, filters };
62}

Buforowanie danych w PPR

Optymalne buforowanie danych jest kluczowe dla wydajności PPR. Oto kilka wzorów buforowania:

1. Buforowanie na poziomie React

1// utils/cache.ts
2export const dataCache = new Map();
3
4export async function getCachedData(key: string, fetcher: () => Promise<any>) {
5  if (!dataCache.has(key)) {
6    try {
7      const promise = fetcher();
8      dataCache.set(key, promise);
9      const data = await promise;
10      return data;
11    } catch (error) {
12      dataCache.delete(key);
13      throw error;
14    }
15  }
16  
17  return dataCache.get(key);
18}

Użycie:

1// components/product-list.tsx
2import { getCachedData } from '@/utils/cache';
3
4async function getProducts(category: string) {
5  return getCachedData(`products-${category}`, () => {
6    return fetch(`https://api.example.com/products?category=${category}`, {
7      cache: 'no-store'
8    }).then(res => res.json());
9  });
10}

2. Memoizacja funkcji pobierających dane

1// lib/fetch-client.ts
2import { cache } from 'react';
3
4// Wykorzystujemy memoizację React do buforowania wyników zapytań
5export const fetchAPI = cache(async (url: string, options: RequestInit = {}) => {
6  const res = await fetch(url, options);
7  
8  if (!res.ok) {
9    throw new Error(`Failed to fetch ${url}`);
10  }
11  
12  return res.json();
13});

Użycie:

1// components/product-data.tsx
2import { fetchAPI } from '@/lib/fetch-client';
3
4export async function ProductData({ id }: { id: string }) {
5  // Zapytanie zostanie wykonane tylko raz na renderowanie, 
6  // nawet jeśli komponent jest używany wielokrotnie z tym samym id
7  const product = await fetchAPI(`https://api.example.com/products/${id}`);
8  
9  return (
10    <div className="product-data">
11      <h2>{product.name}</h2>
12      <p>{product.description}</p>
13      <p className="price">{product.price}</p>
14    </div>
15  );
16}

Przykłady użycia PPR w różnych scenariuszach

1. E-commerce

PPR jest idealny dla stron produktowych e-commerce, gdzie możemy:

  • Prerenderować statyczne elementy: nazwa produktu, obrazy, opis, specyfikacja
  • Dynamicznie renderować: stan magazynowy, ceny promocyjne, recenzje klientów

2. Portale informacyjne

Dla witryn informacyjnych, PPR pozwala na:

  • Prerenderowanie szablonu strony, nagłówków artykułów i stałych sekcji
  • Dynamiczne renderowanie spersonalizowanych rekomendacji i najnowszych wiadomości

3. Dashboardy aplikacji

Dla dashboardów analitycznych:

  • Prerenderowanie struktury dashboard, kontrolek i interfejsu użytkownika
  • Dynamiczne renderowanie wykresów i danych analitycznych specyficznych dla użytkownika

Debugowanie i monitorowanie PPR

Debugging PPR może być wymagający ze względu na hybrydową naturę renderowania. Oto kilka wskazek:

1. Używanie React DevTools

React DevTools pozwala na inspekcję komponentów i ich stanu renderowania.

2. Debugowanie statycznego generowania

1NEXT_DEBUG=1 next build

Ta komenda pokazuje szczegółowe informacje o tym, które strony są generowane statycznie.

3. Monitorowanie środowiska produkcyjnego

Zaimplementuj narzędzia do monitorowania wydajności, takie jak Lighthouse, PageSpeed Insights lub narzędzia monitorowania Core Web Vitals, aby ocenić wpływ PPR na doświadczenie użytkownika końcowego.

Ograniczenia i uwagi

  1. Eksperymentalna funkcja - PPR jest wciąż funkcją eksperymentalną i może ulec zmianie
  2. Wspierane środowiska - PPR działa najlepiej z Node.js i edge runtime
  3. Złożoność modelu mentalnego - hybrydowy model renderowania może być trudniejszy do zrozumienia i debugowania
  4. Optymalne użycie - PPR nie zawsze jest najlepszym wyborem; dla prostych stron statycznych SSG może być prostsze i bardziej wydajne

Najlepsze praktyki

1. Wyważenie statyczne vs dynamiczne treści

Określ jasno, które dane można renderować statycznie, a które muszą być dynamiczne:

  • Statyczne: interfejs użytkownika, stałe metadane, treści rzadko aktualizowane
  • Dynamiczne: spersonalizowane dane, często aktualizowane informacje, dane zależne od kontekstu użytkownika

2. Priorytetyzacja ładowania

Używaj zagłębionego

Suspense
do priorytetyzacji ładowania treści:

  • Krytyczne dla biznesu dane ładuj najpierw
  • Mniej istotne sekcje możesz ładować później

3. Efektywne zarządzanie cache

Starannie zarządzaj cache'owaniem danych, aby uniknąć niepotrzebnych ponownych renderowań:

  • Używaj
    fetchAPI
    z memoizacją React
  • Stosuj walidację cache przy użyciu revalidatePath i revalidateTag

4. Odpowiednie szkielety ładowania

Projektuj szkielety ładowania, które odpowiadają wymiarom i proporcjom końcowej zawartości, aby zminimalizować przeskoki układu.

Podsumowanie

Częściowe Prerenderowanie (PPR) to potężna funkcja Next.js, która umożliwia łączenie wydajności statycznego generowania z elastycznością dynamicznego renderowania. Pozwala to na tworzenie szybkich, interaktywnych aplikacji z optymalną wydajnością dla różnych typów treści.

Główne korzyści PPR to:

  1. Szybsze TTFB - statyczne części są dostarczane natychmiast
  2. Lepsza wydajność SEO - krytyczne treści są statycznie prerendrowane
  3. Optymalne doświadczenie użytkownika - stały szablon minimalizuje migotanie podczas ładowania dynamicznych treści
  4. Elastyczność - łatwe łączenie różnych strategii renderowania w jednej aplikacji

Choć PPR jest wciąż w fazie eksperymentalnej, oferuje ekscytującą wizję przyszłości renderowania w Next.js, łącząc zalety rozproszonych dotąd podejść do generowania treści webowych.

W następnym module omówimy szczegółowo zarządzanie cache i zaawansowane techniki rewalidacji danych, które są kluczowe dla optymalnego działania PPR i innych strategii renderowania w Next.js.

Vai a CodeWorlds