# Partial Prerendering (PPR) > Static CDN shell plus dynamic Suspense holes in one response. Frontend architecture guidance from fearchitect, written by Abas Turabli and last reviewed 2026-06-21. Source: https://fearchitect.com/topics/partial-prerendering Use it as reference for the task at hand. Before changing code, check this guidance against the codebase: where the code already makes a different, deliberate choice, flag the conflict instead of rewriting it. Library APIs move faster than this guide, so confirm exact signatures in the official docs linked at the end. ## Summary PPR serves a prerendered static shell from the CDN instantly, then streams dynamic sections into the same HTTP response as they resolve. Components marked with `use cache` land in the shell; components reading request-time data (`cookies()`, `headers()`) stream in behind ``. Stable in Next.js 16 via `cacheComponents: true`. ## Definition PPR is a rendering model where a single route produces two kinds of output in one response: a **static shell** served from the CDN with zero server wait, and **dynamic holes** that stream in at request time. Before PPR, a route was either fully static (fast, no per-request data) or dynamic (every visitor hit the server). PPR removes that binary. The shell — layout, nav, cached product info — reaches the browser instantly. Personalized or live content streams in behind a `` boundary in the same chunked HTTP response. Set `cacheComponents: true` in `next.config.ts` to enable it. The older `experimental.ppr` flag and `experimental_ppr` route segment were removed in Next.js 16. ## Static shell with a dynamic cart streamed in The static-vs-dynamic boundary is set at the component level. `'use cache'` puts a component in the prerendered shell; reading `cookies()` or `headers()` forces it dynamic and requires a `` wrapper or the build fails. ```tsx // app/product/[id]/page.tsx import { Suspense } from "react"; import { cacheLife } from "next/cache"; // Static: cached product data lands in the shell async function ProductDetail({ id }: { id: string }) { "use cache"; cacheLife("hours"); const product = await db.product.findUnique({ where: { id } }); return
{product?.name}
; } // Dynamic: reads cookies() — must be behind Suspense async function Cart() { const userId = (await cookies()).get("uid")?.value; const cart = await getCart(userId); return ; } export default async function Page({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; return ( <> }> ); } // next.config.ts — one flag enables PPR for the whole App Router // const nextConfig: NextConfig = { cacheComponents: true }; ``` `ProductDetail` is `'use cache'` so it enters the static shell. `Cart` reads `cookies()`, making it dynamic — the `` fallback is in the shell; the real cart streams in at request time. ## Diagram ```mermaid sequenceDiagram participant CDN participant Browser participant Origin Browser->>CDN: GET /product/42 CDN-->>Browser: Static shell (layout + cached data) — instant CDN->>Origin: Forward for dynamic holes Origin-->>Browser: Stream: cart count (cookies) Origin-->>Browser: Stream: live stock (DB query) ``` The CDN serves the static shell immediately; dynamic holes stream from the origin in the same response. ## Trade-offs **Pros** - CDN-speed first byte for every visitor, even on pages with dynamic data. - No all-or-nothing route decision — static and dynamic mix per component. - Suspense fallbacks give instant perceived content without a blank page. - One HTTP response: no client waterfall to fetch the dynamic parts. **Cons** - Requires `cacheComponents: true`; not a drop-in for existing Next.js 15 setups. - Every dynamic component must be wrapped in `` or the build fails. - Unsized Suspense fallbacks cause layout shift when content streams in. - CDN caching logic is more complex — shell and dynamic data have different TTLs. ## Decision: When to use PPR Use PPR when a route has a stable shell (nav, branding, cached product data) plus per-user dynamic sections — e-commerce product pages, dashboards with live widgets. Skip it when the entire page is personalized (no shell to prerender) or when you need to set an HTTP status code or redirect based on request data (the shell is already flushed before that data is available). ## Key terms - **Static shell**: The prerendered HTML served from the CDN, containing cached components and Suspense fallbacks. - **Dynamic hole**: A Suspense boundary whose content reads request-time data and streams in per request. - **`use cache`**: A Next.js directive that caches a component or function's output for inclusion in the static shell. - **`cacheComponents`**: The Next.js 16 config flag that enables PPR as the default rendering model. - **Chunked transfer encoding**: HTTP mechanism that lets the server send a response in pieces, enabling streaming. ## Related topics - [Streaming SSR](https://fearchitect.com/topics/streaming-ssr.md): Flush the HTML shell immediately, then stream the rest as Suspense resolves. - [React Server Components](https://fearchitect.com/topics/react-server-components.md): Server-rendered components that ship zero JS to the browser. - [Rendering Strategies: CSR / SSR / SSG / ISR](https://fearchitect.com/topics/rendering-strategies.md): Where and when HTML is generated: build, request, browser, or revalidated. - [Hydration Strategies & Islands](https://fearchitect.com/topics/hydration-and-islands.md): Pay JS cost only for interactive regions, not the whole page. - [Resumability (Qwik)](https://fearchitect.com/topics/resumability-qwik.md): Skip hydration by serializing state and listeners directly into HTML. ## Further reading - [Next.js — Partial Prerendering (Getting Started)](https://nextjs.org/docs/app/getting-started/partial-prerendering) - [Next.js — cacheComponents config](https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents) - [Next.js — Caching with Cache Components](https://nextjs.org/docs/app/getting-started/caching) - [Next.js — Upgrading to v16 (PPR section)](https://nextjs.org/docs/app/guides/upgrading/version-16#partial-prerendering-ppr)