Skip to content
fearchitect
Rendering & Hydration

Partial Prerendering (PPR)

Static CDN shell plus dynamic Suspense holes in one response.

By Abas TurabliReviewed

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 `<Suspense>`. Stable in Next.js 16 via `cacheComponents: true`.

Jump to the interview angle

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 <Suspense> 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 <Suspense> wrapper or the build fails.

Static shell with a dynamic cart streamed intsx
// 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 <article>{product?.name}</article>;
}

// Dynamic: reads cookies() — must be behind Suspense
async function Cart() {
  const userId = (await cookies()).get("uid")?.value;
  const cart = await getCart(userId);
  return <CartWidget count={cart.items.length} />;
}

export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  return (
    <>
      <ProductDetail id={id} />
      <Suspense fallback={<CartSkeleton />}>
        <Cart />
      </Suspense>
    </>
  );
}

// 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 <Suspense> fallback is in the shell; the real cart streams in at request time.

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 `<Suspense>` 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.

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

Interview angle

Interviewers probe whether you understand the static-vs-dynamic boundary. Key points: reading cookies() or headers() makes a component dynamic and requires <Suspense>; 'use cache' puts it in the static shell. Mention that Next.js 16 made PPR the default via cacheComponents: true, and note the CLS risk from unsized fallbacks.

Soundbite: "PPR streams the CDN shell instantly and fills dynamic holes via Suspense in the same response."

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.

Further reading

Search fearchitect

Jump to a topic, mode, or action.