Kurs Next.js · Moduł 5: Strategie renderowania
Częściowe prerenderowanie (Partial Prerendering)
W tej lekcji11
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:
- Natychmiastowe renderowanie statycznych części strony (np. layout, szablon, nagłówki)
- 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:
- Static Site Generation (SSG) - szybka dostawa treści, ale problemy z aktualnością danych
- Server-Side Rendering (SSR) - zawsze aktualne dane, ale wolniejsze TTFB (Time To First Byte)
- Incremental Static Regeneration (ISR) - kompromis między SSG i SSR, ale z ograniczeniami czasowymi
- 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:
- Statyczne wstępne renderowanie - podczas budowania, Next.js generuje statyczną Shell HTML z "dziurami" dla dynamicznych części
- Natychmiastowa odpowiedź - gdy użytkownik żąda strony, serwer natychmiast odpowiada prerenerowaną treścią Shell
- Dynamiczne wypełnianie - "dziury" są asynchronicznie wypełniane dynamicznymi danymi na serwerze
- 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 16 PPR jest częścią Cache Components i włączasz je jedną opcją w pliku next.config.ts:
1// next.config.ts
2import type { NextConfig } from 'next';
3
4const nextConfig: NextConfig = {
5 cacheComponents: true,
6};
7
8export default nextConfig;Po włączeniu tej opcji PPR działa na każdej trasie: do statycznej powłoki trafia wszystko, co da się wyrenderować przy budowaniu, łącznie z danymi z funkcji oznaczonych dyrektywą 'use cache', a dynamiczne fragmenty owinięte w <Suspense> są dosyłane przy żądaniu. W Next.js 14 i 15 PPR było eksperymentalne (w Next.js 15 tylko w wersjach canary) i włączało się flagą experimental.ppr, którą Next.js 16 usunął:
1// next.config.js - Next.js 14-15, w Next.js 16 ta flaga już nie działa
2module.exports = {
3 experimental: {
4 ppr: true
5 }
6}W tamtych wersjach PPR można też było włączyć tylko dla wybranych tras eksportem experimental_ppr (przy ustawieniu ppr: 'incremental'). Next.js 16 usunął również ten eksport, a przy włączonym cacheComponents opcje segmentu, takie jak dynamic, dynamicParams, revalidate i fetchCache, przestają być dostępne:
1// Next.js 15 canary (usunięte w Next.js 16)
2export const experimental_ppr = true;2. Podstawowy przykład PPR
Oto przykład implementacji PPR w Next.js 16 (z włączonym cacheComponents):
1// app/products/[category]/page.tsx
2import { Suspense } from 'react';
3import { cacheLife } from 'next/cache';
4import { CategoryHeader } from '@/components/category-header';
5import { ProductGrid } from '@/components/product-grid';
6import { ProductFilters } from '@/components/product-filters';
7import { LoadingSkeleton } from '@/components/loading-skeleton';
8
9// Statyczne parametry dla prerendrowanych stron
10export async function generateStaticParams() {
11 const categories = await fetchTopCategories();
12 return categories.map(category => ({ category: category.slug }));
13}
14
15// Statyczne dane, które są znane podczas budowania
16async function getCategoryData(categorySlug: string) {
17 'use cache';
18 cacheLife('days');
19
20 // Pobierz podstawowe dane kategorii, które rzadko się zmieniają.
21 // Dzięki 'use cache' dane zostaną pobrane podczas budowania
22 // i będą częścią statycznego Shella
23 const data = await fetch(`https://api.example.com/categories/${categorySlug}`);
24
25 return data.json();
26}
27
28export default async function CategoryPage({
29 params,
30 searchParams,
31}: {
32 params: Promise<{ category: string }>;
33 searchParams: Promise<Record<string, string>>;
34}) {
35 // Pobierz statyczne dane kategorii (renderowane podczas budowania)
36 const categoryData = await getCategoryData((await params).category);
37
38 return (
39 <div className="container mx-auto py-8">
40 {/* Statycznie renderowany nagłówek - część Shella */}
41 <CategoryHeader category={categoryData} />
42
43 <div className="grid grid-cols-4 gap-6 mt-8">
44 <div className="col-span-1">
45 {/* Dynamicznie renderowane filtry z danymi zależnymi od sesji użytkownika */}
46 <Suspense fallback={<LoadingSkeleton type="filters" />}>
47 <ProductFilters categorySlug={(await params).category} />
48 </Suspense>
49 </div>
50
51 <div className="col-span-3">
52 {/* Dynamicznie renderowana lista produktów ze świeżymi danymi */}
53 <Suspense fallback={<LoadingSkeleton type="products" />}>
54 <ProductGrid categorySlug={(await params).category} searchParams={searchParams} />
55 </Suspense>
56 </div>
57 </div>
58 </div>
59 );
60}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({
18 categorySlug,
19 searchParams,
20}: {
21 categorySlug: string;
22 searchParams: Promise<Record<string, string>>;
23}) {
24 // Parametry filtrowania z URL odczytujemy dopiero tutaj, wewnątrz <Suspense>
25 const filterParams = await searchParams;
26
27 // Pobierz produkty z uwzględnieniem filtrów
28 const products = await getProducts(categorySlug, filterParams);
29
30 return (
31 <div className="grid grid-cols-3 gap-4">
32 {products.map(product => (
33 <ProductCard key={product.id} product={product} />
34 ))}
35 </div>
36 );
37}Zwróć uwagę, że searchParams trafia do ProductGrid jako obietnica i jest odczytywane dopiero wewnątrz granicy <Suspense>. Dzięki temu nagłówek kategorii i szkielety ładowania zostają w statycznej powłoce, a dynamiczna jest tylko lista produktów. Hooka useSearchParams nie użyjesz tu w ogóle, bo działa wyłącznie w komponentach klienckich.
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} zł</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} zł</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 buildTa 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
- Eksperymentalna funkcja - PPR jest wciąż funkcją eksperymentalną i może ulec zmianie
- Wspierane środowiska - PPR działa najlepiej z Node.js i edge runtime
- Złożoność modelu mentalnego - hybrydowy model renderowania może być trudniejszy do zrozumienia i debugowania
- 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
fetchAPIz 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:
- Szybsze TTFB - statyczne części są dostarczane natychmiast
- Lepsza wydajność SEO - krytyczne treści są statycznie prerendrowane
- Optymalne doświadczenie użytkownika - stały szablon minimalizuje migotanie podczas ładowania dynamicznych treści
- 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.
Kod do tej lekcji: App.tsx
1import React, { useState, useEffect } from 'react';
2
3// Partial Prerendering (PPR) - symulacja w Next.js
4// Czesc statyczna renderowana natychmiast, dynamiczna streamowana pozniej
5
6function StaticShell() {
7 return (
8 <div style={{ background: '#1a2744', borderRadius: 8, padding: 16, border: '1px solid #64ffda' }}>
9 <h3 style={{ color: '#64ffda', margin: 0 }}>Statyczny shell (prerendered)</h3>
10 <p style={{ color: '#aaa', fontSize: 13, margin: '8px 0 0' }}>
11 Nawigacja, layout i metadane sa dostepne natychmiast.
12 Ta czesc strony jest wygenerowana w build time.
13 </p>
14 </div>
15 );
16}
17
18function DynamicSection({ label, delay }: { label: string; delay: number }) {
19 const [data, setData] = useState<string | null>(null);
20
21 useEffect(() => {
22 const t = setTimeout(() => setData(`Dane dynamiczne: ${label} (zaladowane po ${delay}ms)`), delay);
23 return () => clearTimeout(t);
24 }, [label, delay]);
25
26 if (!data) {
27 return (
28 <div style={{ background: '#0d1117', borderRadius: 8, padding: 16, border: '1px dashed #555' }}>
29 <div style={{ color: '#888', fontSize: 13 }}>Streaming: {label}...</div>
30 <div style={{
31 height: 10, background: 'linear-gradient(90deg, #333, #555, #333)',
32 backgroundSize: '200% 100%', borderRadius: 4, marginTop: 8,
33 animation: 'shimmer 1.5s ease-in-out infinite'
34 }} />
35 </div>
36 );
37 }
38
39 return (
40 <div style={{ background: '#1b5e20', borderRadius: 8, padding: 16, border: '1px solid #4caf50' }}>
41 <div style={{ color: '#4caf50', fontWeight: 'bold', fontSize: 13 }}>DYNAMIC</div>
42 <div style={{ color: '#e0e0e0', marginTop: 4 }}>{data}</div>
43 </div>
44 );
45}
46
47export default function PPRDemo() {
48 const [key, setKey] = useState(0);
49
50 return (
51 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: 20, color: '#e0e0e0', fontFamily: 'monospace' }}>
52 <style>{`@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }`}</style>
53 <h1 style={{ color: '#64ffda' }}>Partial Prerendering (PPR)</h1>
54 <p style={{ color: '#888' }}>
55 Shell statyczny laduje sie natychmiast, dynamiczne sekcje streamuja sie niezaleznie.
56 </p>
57 <button onClick={() => setKey(k => k + 1)} style={{
58 background: '#9c27b0', color: '#fff', border: 'none', borderRadius: 6,
59 padding: '8px 16px', cursor: 'pointer', margin: '12px 0', fontFamily: 'monospace'
60 }}>
61 Symuluj ponowne ladowanie
62 </button>
63
64 <div key={key} style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
65 <StaticShell />
66 <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
67 <DynamicSection label="Koszyk uzytkownika" delay={800} />
68 <DynamicSection label="Rekomendacje" delay={2000} />
69 </div>
70 <DynamicSection label="Komentarze spolecznosci" delay={3000} />
71 </div>
72 </div>
73 );
74}Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Streaming w Next.js pozwala na:
2. Suspense boundaries w React służą do:
Zadania praktyczne w grze
- Edytor kodu
Utwórz loading.tsx i error.tsx components dla graceful handling asynchronicznych operacji
- Układanie w pionie
Ułóż etapy rewalidacji ISR (Incremental Static Regeneration) w prawidłowej kolejności:
- Klikanie w kolejności
Ułóż składnię funkcji generateStaticParams w Next.js
- Edytor kodu
Utwórz aplikację łączącą Server Components (header, footer) z Client Components (search, cart) w jednej stronie