Next.js Curriculum
MediumRendering Paradigms100% Free30 min read

Partial Prerendering (PPR) & Cache Components

The 30-Second Interview Answer

Partial Prerendering (PPR) combines static shell pre-rendering with dynamic streaming slots within a single HTTP response. At build time, Next.js generates static HTML for static segments and punches holes around dynamic Suspense boundaries. At request time, the static shell is served instantly from edge cache while dynamic slots stream in parallel.

Learning Objectives

Explain how Partial Prerendering delivers static CDN performance with real-time dynamic data.

Identify how Next.js 16 consolidates PPR and "use cache" into the Cache Components model.

Configure experimental PPR in Next.js 15 (experimental.ppr) and Cache Components in Next.js 16.

Analyze the network waterfall of a PPR response compared to separate client fetch round-trips.

Prerequisites & Background Knowledge
Streaming SSRStatic Site GenerationSuspense boundaries

Why Interviewers Care

High-signal architectural topic for senior engineering interviews at cutting-edge tech companies.

The Challenge

How does Partial Prerendering (PPR) eliminate the historical trade-off between static edge caching and dynamic per-request rendering? Contrast PPR with traditional client-side data fetching after initial load.

Code Architecture & Implementation

1// next.config.ts (Next.js 15 experimental flag)
2// experimental: { ppr: 'incremental' }
3
4import { Suspense } from 'react';
5import { cookies } from 'next/headers';
6
7// Opt in to PPR for this route segment
8export const experimental_ppr = true;
9
10export default function EcommerceProductPage() {
11 return (
12 <main className="product-page">
13 {/* 1. STATIC SHELL: Generated at build time, cached globally at CDN */}
14 <section className="product-hero">
15 <h1>Flagship Ergonomic Chair</h1>
16 <p>Premium aluminum base with breathable mesh backing.</p>
17 <img src="/chair.jpg" alt="Ergonomic Chair" width={600} height={400} />
18 </section>
19
20 {/* 2. DYNAMIC HOLE: Fallback included in static shell, dynamic slot streams in */}
21 <Suspense fallback={<CartBadgeSkeleton />}>
22 <DynamicUserCartState />
23 </Suspense>
24
25 <Suspense fallback={<PriceAndInventorySkeleton />}>
26 <RealtimeInventoryPrice />
27 </Suspense>
28 </main>
29 );
30}
31
32// Dynamic Server Component accessing cookies
33async function DynamicUserCartState() {
34 const cookieStore = await cookies();
35 const cartId = cookieStore.get('cart_id')?.value;
36 const cart = await fetch(`https://api.store.com/cart/${cartId}`, { cache: 'no-store' }).then(r => r.json());
37
38 return <div className="cart-badge">Cart Items: {cart.totalItems}</div>;
39}
40
41// Dynamic Server Component checking live stock
42async function RealtimeInventoryPrice() {
43 const stock = await fetch('https://api.store.com/stock/chair-1', { cache: 'no-store' }).then(r => r.json());
44 return <div className="stock-status">In Stock: {stock.quantity} left ({stock.currentPrice})</div>;
45}

Architectural Explanation

The Historical Dilemma

Historically, web architectures forced an agonizing choice:

  1. Option A (Pure SSR): Real-time data, but slow TTFB (150–500ms) because the entire page waits for the slowest database query.
  2. Option B (SSG + Client Fetch): Instant static shell (<30ms), but dynamic user data requires waiting for client JavaScript to hydrate, followed by client-side REST/GraphQL round-trips. This causes layout shifts, spinner fatigue, and network waterfalls.

The Solution: Partial Prerendering (PPR)

PPR delivers the best of both worlds in a single unified HTTP connection:

  • At Build Time: Next.js pre-renders all static layout and components. When it encounters a dynamic <Suspense> boundary, it creates a "hole" containing the static fallback skeleton. This shell is saved as static HTML.
  • At Request Time:
    1. The static HTML shell is sent from the Edge CDN immediately ($<20$ms TTFB).
    2. The server concurrently begins executing the dynamic Server Components inside the holes.
    3. The dynamic data streams into the holes through the same HTTP connection as soon as it resolves.

Next.js 16 Evolution: Cache Components

In Next.js 16, PPR and function-level caching were unified into Cache Components. Rather than treating PPR as an isolated page-level experimental flag, caching is declarative: any component marked with "use cache" participates in static prerendering, while un-cached components stream dynamically.

Common Mistakes & Anti-Patterns

Assuming PPR requires two separate HTTP requests from the browser (it is a single streaming HTTP response).

Thinking client-side useEffect fetching is identical to PPR (client fetching adds round-trip latency and hydration delays).

Forgetting that dynamic functions (cookies(), headers()) must be wrapped in Suspense boundaries for the parent layout to remain prerendered.

Interview Follow-Up Traps

Interviewer asks: "Is Partial Prerendering stable in Next.js 15?" Trap answer: Yes, it is the default. Correct answer: False. In Next.js 15, PPR is experimental and must be enabled via experimental: { ppr: "incremental" } in next.config. Next.js 16 consolidates it into the Cache Components model.

Interviewer asks: "What happens if a component outside of Suspense calls cookies() when PPR is enabled?" Trap answer: Next.js creates an automatic Suspense wrapper. Correct answer: The entire page bails out of static prerendering and falls back to dynamic request-time rendering.

Frequently Asked Questions

What is the primary benefit of PPR over traditional static sites with client fetching?

PPR eliminates client-side network waterfalls and layout shifts by streaming dynamic data directly from the server in the initial HTTP response without waiting for client JavaScript hydration.

Set experimental: { ppr: "incremental" } in next.config.ts, and export const experimental_ppr = true in the target page or layout file.

Any Server Component that reads dynamic request data (cookies, headers, searchParams) or calls uncached APIs inside a React Suspense boundary.

Next.js 16 unifies PPR with the "use cache" directive into Cache Components, making caching declarative and composable across components and functions.

Yes. When combined with generateStaticParams, the static shell is pre-rendered for known parameters while streaming user-specific data dynamically.