React 19.2 introduces a revolutionary feature called Partial Pre-rendering (PPR), which combines the best features of static and dynamic rendering. Imagine a spaceship that already has a pre-calculated route to many destinations, but can dynamically adjust course when new data appears -- that is exactly how PPR works!
PPR allows:
1+------------------------------------------+
2| Web Page |
3+------------------------------------------+
4| +----------------------------------+ |
5| | HEADER (pre-rendered) | | <- Instant
6| +----------------------------------+ |
7| +----------------------------------+ |
8| | SIDEBAR (pre-rendered) | | <- Instant
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, // Enable 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// This page will be partially pre-rendered
9export default function MissionsPage() {
10 return (
11 <div className="missions-layout">
12 {/* These components are static - pre-rendered */}
13 <MissionHeader />
14 <MissionSidebar />
15
16 {/* This component is dynamic - streaming */}
17 <Suspense fallback={<MissionListSkeleton />}>
18 <MissionList />
19 </Suspense>
20 </div>
21 );
22}1// MissionHeader.jsx - static component
2export default function MissionHeader() {
3 return (
4 <header className="mission-header">
5 <h1>Mission Control Center</h1>
6 <nav>
7 <a href="/missions">All Missions</a>
8 <a href="/missions/active">Active</a>
9 <a href="/missions/completed">Completed</a>
10 </nav>
11 </header>
12 );
13}
14
15// MissionSidebar.jsx - static component
16export default function MissionSidebar() {
17 return (
18 <aside className="mission-sidebar">
19 <h2>Filter Missions</h2>
20 <ul>
21 <li>Exploration missions</li>
22 <li>Research missions</li>
23 <li>Rescue missions</li>
24 </ul>
25 </aside>
26 );
27}1// MissionList.jsx - dynamic Server Component
2async function getMissions() {
3 // This function executes during request time
4 const res = await fetch('https://api.space-missions.com/missions', {
5 cache: 'no-store', // Always fresh data
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 during loading
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}1Without PPR:
2Request -> Render everything -> Response
3 [==========] 800ms
4
5With PPR:
6Request -> Pre-rendered shell -> Response (instant)
7 Dynamic parts -> Stream
8 [==] 50ms (shell)
9 [======] 750ms (dynamic, streaming)1// Page with multiple dynamic sections
2export default function DashboardPage() {
3 return (
4 <div className="dashboard">
5 {/* Static layout - instant LCP */}
6 <DashboardHeader />
7 <DashboardNav />
8
9 <main className="dashboard-content">
10 {/* Dynamic widgets - 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 {/* Level 1: Main mission data */}
5 <Suspense fallback={<MissionHeaderSkeleton />}>
6 <MissionHeader missionId={params.id} />
7
8 {/* Level 2: Crew (depends on mission data) */}
9 <Suspense fallback={<CrewListSkeleton />}>
10 <CrewList missionId={params.id} />
11 </Suspense>
12
13 {/* Level 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// Static components
5function DashboardLayout({ children }) {
6 return (
7 <div className="space-dashboard">
8 <header className="dashboard-header">
9 <h1>Galactic Control Center</h1>
10 <time>{new Date().toLocaleDateString('en-US')}</time>
11 </header>
12 {children}
13 </div>
14 );
15}
16
17// Dynamic component - fetches real-time data
18async function LiveMissionFeed() {
19 const missions = await fetch('https://api.space/missions/live', {
20 next: { revalidate: 0 } // Always fresh
21 }).then(r => r.json());
22
23 return (
24 <section className="live-feed">
25 <h2>Active Missions</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>Progress: {m.progress}%</p>
31 </article>
32 ))}
33 </section>
34 );
35}
36
37// Dynamic component - notifications
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>Notifications</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// Static component - general statistics
56function GeneralStats() {
57 return (
58 <div className="stats-panel">
59 <h2>Fleet Statistics</h2>
60 <ul>
61 <li>Active ships: 47</li>
62 <li>Completed missions: 1,234</li>
63 <li>Crew members: 892</li>
64 </ul>
65 </div>
66 );
67}
68
69// Main page with PPR
70export default function DashboardPage() {
71 return (
72 <DashboardLayout>
73 {/* Static - pre-rendered */}
74 <GeneralStats />
75
76 <div className="dynamic-content">
77 {/* Dynamic - 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 decides)
8export const dynamic = 'auto';
9
10// Usage example
11// app/missions/[id]/page.jsx
12export const dynamic = 'auto'; // PPR enabled
13
14export default async function MissionPage({ params }) {
15 // Static parts will be pre-rendered
16 // Dynamic parts (with Suspense) will stream
17 return (
18 <div>
19 <StaticMissionInfo />
20 <Suspense fallback={<Loading />}>
21 <DynamicMissionData id={params.id} />
22 </Suspense>
23 </div>
24 );
25}| Scenario | Recommendation | |----------|---------------| | Landing page with dynamic hero | PPR | | Dashboard with real-time data | PPR | | E-commerce with prices/availability | PPR | | Blog with static content | Full SSG | | SPA without SEO | Client-side | | Page with personalization | PPR |
1+----------------+-----------------------------------------+
2| Strategy | Characteristics |
3+----------------+-----------------------------------------+
4| SSG | Entirely pre-rendered, no dynamics |
5| SSR | Entirely rendered per-request |
6| ISR | Pre-rendered with revalidation |
7| CSR | Entirely rendered on client |
8| PPR | Static shell + dynamic streaming |
9+----------------+-----------------------------------------+
10
11SSG: [████████████] -> Client (instant, but stale)
12SSR: [............] -> [████████████] -> Client (slower)
13PPR: [████] -> [░░░░░░░░] -> Client (fast shell + streaming)1// Check which parts are pre-rendered
2export default function DebugPage() {
3 return (
4 <div>
5 {/* This section will show render time */}
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)); // Simulate delay
21
22 return (
23 <div data-ppr-debug>
24 Dynamic rendered at: {new Date().toISOString()}
25 </div>
26 );
27}Partial Pre-rendering in React 19.2 is a breakthrough feature:
PPR is like a hybrid drive on a spaceship -- combining the reliability of proven solutions with the flexibility of modern technologies!