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!
PPR pozwala na:
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└─────────────────────────────────────────┘1// next.config.js
2module.exports = {
3 experimental: {
4 ppr: true, // Włącz Partial Pre-rendering
5 },
6};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}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}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}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)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}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}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}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}| 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 |
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)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}Partial Pre-rendering w React 19.2 to przełomowa funkcja:
PPR to jak hybrydowy napęd w statku kosmicznym - łączy niezawodność sprawdzonych rozwiązań z elastycznością nowoczesnych technologii!