We use cookies to enhance your experience on the site
CodeWorlds

React 19.2 - Partial Pre-rendering (PPR)

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!

What Is Partial Pre-rendering?

PPR allows:

  • Pre-rendering static parts of a page during build time
  • Dynamic rendering of the rest during request time
  • Streaming dynamic parts to the client as they become ready
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+------------------------------------------+

How Does PPR Work?

1. Configuration in Next.js

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

2. Page Structure with 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// 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}

3. Static Components (pre-rendered)

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}

4. Dynamic Components (streaming)

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}

PPR Advantages

1. Faster Time To First Byte (TTFB)

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)

2. Better Core Web Vitals

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}

3. Hierarchical Suspense

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}

Practical Example - Space Dashboard

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}

Controlling Cache Behavior

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}

When to Use PPR?

| 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 |

Comparing Rendering Strategies

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)

Debugging PPR

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}

Summary

Partial Pre-rendering in React 19.2 is a breakthrough feature:

  1. Best of both worlds - speed of static + freshness of dynamic
  2. Automatic optimizations - React/Next.js decides what to pre-render
  3. HTML streaming - user sees content progressively
  4. Suspense boundaries - you control the granularity of dynamic parts
  5. Better Core Web Vitals - especially LCP and FCP

PPR is like a hybrid drive on a spaceship -- combining the reliability of proven solutions with the flexibility of modern technologies!

Go to CodeWorlds