Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

React 19.2 - Partial Pre-rendering (PPR)

React 19.2 wprowadza rewolucyjną funkcję zwaną Partial Pre-rendering (PPR), która łączy najlepsze cechy renderowania statycznego i dynamicznego. Wyobraź sobie statek kosmiczny, który ma już wstępnie obliczoną trasę do wielu destynacji, ale może dynamicznie dostosować kurs gdy pojawią się nowe dane - tak właśnie działa PPR!

Czym jest Partial Pre-rendering?

PPR pozwala na:

  • Pre-renderowanie statycznych części strony podczas build time
  • Dynamiczne renderowanie reszty podczas request time
  • Streaming dynamicznych części do klienta w miarę ich gotowości
1┌─────────────────────────────────────────┐
2│           Strona WWW                     │
3├─────────────────────────────────────────┤
4│  ┌─────────────────────────────────┐    │
5│  │    NAGŁÓWEK (pre-rendered)       │    │ ← Natychmiastowo
6│  └─────────────────────────────────┘    │
7│  ┌─────────────────────────────────┐    │
8│  │    SIDEBAR (pre-rendered)        │    │ ← Natychmiastowo
9│  └─────────────────────────────────┘    │
10│  ┌─────────────────────────────────┐    │
11│  │    CONTENT (Suspense boundary)   │    │ ← Streaming
12│  │    ┌───────────────────────┐     │    │
13│  │    │  Loading...           │     │    │
14│  │    │  → Dynamic content    │     │    │
15│  │    └───────────────────────┘     │    │
16│  └─────────────────────────────────┘    │
17└─────────────────────────────────────────┘

Jak działa PPR?

1. Konfiguracja w Next.js

1// next.config.js
2module.exports = {
3  experimental: {
4    ppr: true, // Włącz Partial Pre-rendering
5  },
6};

2. Struktura strony z PPR

1// app/missions/page.jsx
2import { Suspense } from 'react';
3import MissionHeader from './MissionHeader';
4import MissionSidebar from './MissionSidebar';
5import MissionList from './MissionList';
6import MissionListSkeleton from './MissionListSkeleton';
7
8// Ta strona będzie częściowo pre-renderowana
9export default function MissionsPage() {
10  return (
11    <div className="missions-layout">
12      {/* Te komponenty są statyczne - pre-rendered */}
13      <MissionHeader />
14      <MissionSidebar />
15
16      {/* Ten komponent jest dynamiczny - streaming */}
17      <Suspense fallback={<MissionListSkeleton />}>
18        <MissionList />
19      </Suspense>
20    </div>
21  );
22}

3. Komponenty statyczne (pre-rendered)

1// MissionHeader.jsx - statyczny komponent
2export default function MissionHeader() {
3  return (
4    <header className="mission-header">
5      <h1>Centrum Kontroli Misji</h1>
6      <nav>
7        <a href="/missions">Wszystkie misje</a>
8        <a href="/missions/active">Aktywne</a>
9        <a href="/missions/completed">Ukończone</a>
10      </nav>
11    </header>
12  );
13}
14
15// MissionSidebar.jsx - statyczny komponent
16export default function MissionSidebar() {
17  return (
18    <aside className="mission-sidebar">
19      <h2>Filtruj misje</h2>
20      <ul>
21        <li>Misje eksploracyjne</li>
22        <li>Misje badawcze</li>
23        <li>Misje ratunkowe</li>
24      </ul>
25    </aside>
26  );
27}

4. Komponenty dynamiczne (streaming)

1// MissionList.jsx - dynamiczny Server Component
2async function getMissions() {
3  // Ta funkcja wykonuje się podczas request time
4  const res = await fetch('https://api.space-missions.com/missions', {
5    cache: 'no-store', // Zawsze świeże dane
6  });
7  return res.json();
8}
9
10export default async function MissionList() {
11  const missions = await getMissions();
12
13  return (
14    <div className="mission-list">
15      {missions.map(mission => (
16        <MissionCard key={mission.id} mission={mission} />
17      ))}
18    </div>
19  );
20}
21
22// MissionListSkeleton.jsx - fallback podczas ładowania
23export default function MissionListSkeleton() {
24  return (
25    <div className="mission-list skeleton">
26      {[1, 2, 3].map(i => (
27        <div key={i} className="mission-card-skeleton">
28          <div className="skeleton-title" />
29          <div className="skeleton-text" />
30          <div className="skeleton-text short" />
31        </div>
32      ))}
33    </div>
34  );
35}

Zalety PPR

1. Szybszy Time To First Byte (TTFB)

1Bez PPR:
2Request → Render całości → Response
3         [==========] 800ms
4
5Z PPR:
6Request → Pre-rendered shell → Response (instant)
7          Dynamic parts → Stream
8         [==] 50ms (shell)
9             [======] 750ms (dynamic, streaming)

2. Lepsze Core Web Vitals

1// Strona z wieloma dynamicznymi sekcjami
2export default function DashboardPage() {
3  return (
4    <div className="dashboard">
5      {/* Statyczny layout - natychmiastowy LCP */}
6      <DashboardHeader />
7      <DashboardNav />
8
9      <main className="dashboard-content">
10        {/* Dynamiczne widgety - streaming */}
11        <Suspense fallback={<WidgetSkeleton />}>
12          <ActiveMissionsWidget />
13        </Suspense>
14
15        <Suspense fallback={<WidgetSkeleton />}>
16          <CrewStatusWidget />
17        </Suspense>
18
19        <Suspense fallback={<WidgetSkeleton />}>
20          <ResourcesWidget />
21        </Suspense>
22      </main>
23    </div>
24  );
25}

3. Hierarchiczny Suspense

1export default function MissionDetailsPage({ params }) {
2  return (
3    <div className="mission-details">
4      {/* Poziom 1: Główne dane misji */}
5      <Suspense fallback={<MissionHeaderSkeleton />}>
6        <MissionHeader missionId={params.id} />
7
8        {/* Poziom 2: Załoga (zależy od danych misji) */}
9        <Suspense fallback={<CrewListSkeleton />}>
10          <CrewList missionId={params.id} />
11        </Suspense>
12
13        {/* Poziom 2: Timeline */}
14        <Suspense fallback={<TimelineSkeleton />}>
15          <MissionTimeline missionId={params.id} />
16        </Suspense>
17      </Suspense>
18    </div>
19  );
20}

Praktyczny przykład - Dashboard kosmiczny

1// app/dashboard/page.jsx
2import { Suspense } from 'react';
3
4// Statyczne komponenty
5function DashboardLayout({ children }) {
6  return (
7    <div className="space-dashboard">
8      <header className="dashboard-header">
9        <h1>🚀 Galaktyczne Centrum Kontroli</h1>
10        <time>{new Date().toLocaleDateString('pl-PL')}</time>
11      </header>
12      {children}
13    </div>
14  );
15}
16
17// Dynamiczny komponent - pobiera dane w czasie rzeczywistym
18async function LiveMissionFeed() {
19  const missions = await fetch('https://api.space/missions/live', {
20    next: { revalidate: 0 } // Zawsze świeże
21  }).then(r => r.json());
22
23  return (
24    <section className="live-feed">
25      <h2>Aktywne misje</h2>
26      {missions.map(m => (
27        <article key={m.id} className="mission-item">
28          <h3>{m.name}</h3>
29          <p>Status: {m.status}</p>
30          <p>Postęp: {m.progress}%</p>
31        </article>
32      ))}
33    </section>
34  );
35}
36
37// Dynamiczny komponent - powiadomienia
38async function NotificationsPanel() {
39  const notifications = await fetch('https://api.space/notifications', {
40    next: { revalidate: 0 }
41  }).then(r => r.json());
42
43  return (
44    <aside className="notifications">
45      <h2>Powiadomienia</h2>
46      {notifications.map(n => (
47        <div key={n.id} className={`notification ${n.priority}`}>
48          {n.message}
49        </div>
50      ))}
51    </aside>
52  );
53}
54
55// Statyczny komponent - statystyki ogólne
56function GeneralStats() {
57  return (
58    <div className="stats-panel">
59      <h2>Statystyki floty</h2>
60      <ul>
61        <li>Aktywne statki: 47</li>
62        <li>Ukończone misje: 1,234</li>
63        <li>Członkowie załóg: 892</li>
64      </ul>
65    </div>
66  );
67}
68
69// Główna strona z PPR
70export default function DashboardPage() {
71  return (
72    <DashboardLayout>
73      {/* Statyczne - pre-rendered */}
74      <GeneralStats />
75
76      <div className="dynamic-content">
77        {/* Dynamiczne - streaming */}
78        <Suspense fallback={<div className="skeleton feed-skeleton" />}>
79          <LiveMissionFeed />
80        </Suspense>
81
82        <Suspense fallback={<div className="skeleton notif-skeleton" />}>
83          <NotificationsPanel />
84        </Suspense>
85      </div>
86    </DashboardLayout>
87  );
88}

Kontrolowanie zachowania cache

1// Force static (pre-rendered)
2export const dynamic = 'force-static';
3
4// Force dynamic (no pre-rendering)
5export const dynamic = 'force-dynamic';
6
7// Auto (PPR decyduje)
8export const dynamic = 'auto';
9
10// Przykład użycia
11// app/missions/[id]/page.jsx
12export const dynamic = 'auto'; // PPR włączone
13
14export default async function MissionPage({ params }) {
15  // Statyczne części będą pre-rendered
16  // Dynamiczne części (z Suspense) będą streaming
17  return (
18    <div>
19      <StaticMissionInfo />
20      <Suspense fallback={<Loading />}>
21        <DynamicMissionData id={params.id} />
22      </Suspense>
23    </div>
24  );
25}

Kiedy używać PPR?

| Scenariusz | Rekomendacja | |------------|--------------| | Landing page z dynamicznym hero | ✅ PPR | | Dashboard z real-time data | ✅ PPR | | E-commerce z cenami/dostępnością | ✅ PPR | | Blog ze statyczną treścią | ❌ Pełny SSG | | Aplikacja SPA bez SEO | ❌ Client-side | | Strona z personalizacją | ✅ PPR |

Porównanie strategii renderowania

1┌────────────────┬─────────────────────────────────────────┐
2│ Strategia      │ Charakterystyka                          │
3├────────────────┼─────────────────────────────────────────┤
4│ SSG            │ Całość pre-rendered, brak dynamiki       │
5│ SSR            │ Całość renderowana per-request           │
6│ ISR            │ Pre-rendered z revalidation              │
7│ CSR            │ Całość renderowana na kliencie           │
8│ PPR            │ Statyczny shell + dynamiczny streaming   │
9└────────────────┴─────────────────────────────────────────┘
10
11SSG:   [████████████] → Client (instant, but stale)
12SSR:   [............] → [████████████] → Client (slower)
13PPR:   [████] → [░░░░░░░░] → Client (fast shell + streaming)

Debugging PPR

1// Sprawdź, które części są pre-rendered
2export default function DebugPage() {
3  return (
4    <div>
5      {/* Ta sekcja pokaże czas renderowania */}
6      <div data-ppr-debug>
7        Rendered at: {new Date().toISOString()}
8        <br />
9        Type: {typeof window === 'undefined' ? 'Server' : 'Client'}
10      </div>
11
12      <Suspense fallback={<div>Loading dynamic...</div>}>
13        <DynamicSection />
14      </Suspense>
15    </div>
16  );
17}
18
19async function DynamicSection() {
20  await new Promise(r => setTimeout(r, 1000)); // Symuluj opóźnienie
21
22  return (
23    <div data-ppr-debug>
24      Dynamic rendered at: {new Date().toISOString()}
25    </div>
26  );
27}

Podsumowanie

Partial Pre-rendering w React 19.2 to przełomowa funkcja:

  1. Najlepsze z dwóch światów - szybkość statycznego + świeżość dynamicznego
  2. Automatyczne optymalizacje - React/Next.js decyduje co pre-renderować
  3. Streaming HTML - użytkownik widzi treść stopniowo
  4. Suspense boundaries - kontrolujesz granularność dynamicznych części
  5. Lepsze Core Web Vitals - szczególnie LCP i FCP

PPR to jak hybrydowy napęd w statku kosmicznym - łączy niezawodność sprawdzonych rozwiązań z elastycznością nowoczesnych technologii!

Vai a CodeWorlds