- Next.js
- Next.js Rendering Foundations
- Partial Prerendering (PPR) & Cache Components
Partial Prerendering (PPR) & Cache Components
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
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' }34import { Suspense } from 'react';5import { cookies } from 'next/headers';67// Opt in to PPR for this route segment8export const experimental_ppr = true;910export 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>1920 {/* 2. DYNAMIC HOLE: Fallback included in static shell, dynamic slot streams in */}21 <Suspense fallback={<CartBadgeSkeleton />}>22 <DynamicUserCartState />23 </Suspense>2425 <Suspense fallback={<PriceAndInventorySkeleton />}>26 <RealtimeInventoryPrice />27 </Suspense>28 </main>29 );30}3132// Dynamic Server Component accessing cookies33async 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());3738 return <div className="cart-badge">Cart Items: {cart.totalItems}</div>;39}4041// Dynamic Server Component checking live stock42async 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:
- Option A (Pure SSR): Real-time data, but slow TTFB (150–500ms) because the entire page waits for the slowest database query.
- 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:
- The static HTML shell is sent from the Edge CDN immediately ($<20$ms TTFB).
- The server concurrently begins executing the dynamic Server Components inside the holes.
- 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
How do you enable PPR incrementally on specific pages in Next.js 15?
Set experimental: { ppr: "incremental" } in next.config.ts, and export const experimental_ppr = true in the target page or layout file.
What components are considered dynamic holes in PPR?
Any Server Component that reads dynamic request data (cookies, headers, searchParams) or calls uncached APIs inside a React Suspense boundary.
How does Next.js 16 change the PPR mental model?
Next.js 16 unifies PPR with the "use cache" directive into Cache Components, making caching declarative and composable across components and functions.
Can you use PPR with dynamic route parameters?
Yes. When combined with generateStaticParams, the static shell is pre-rendered for known parameters while streaming user-specific data dynamically.
