Partial Prerendering

PPR combines a static shell with streamed dynamic content in a single route.

Partial Prerendering (PPR) lets one page be mostly static while still having dynamic, per-request parts. Next serves the static shell instantly from the edge, then streams in the dynamic sections wrapped in <Suspense>. This removes the old all-or-nothing choice between static and dynamic.

How it works

  • Everything outside a Suspense boundary is prerendered as the static shell.
  • Anything inside a Suspense boundary that reads request data becomes a dynamic hole, streamed in.

Example

Example · typescript
import { Suspense } from 'react';

export default function ProductPage() {
  return (
    <main>
      <StaticHeader />           {/* prerendered shell */}
      <Suspense fallback={<CartSkeleton />}>
        <Cart />                 {/* dynamic, per-user, streamed */}
      </Suspense>
    </main>
  );
}

When to use it

  • A product page statically renders the header, navigation, and price while the personalised recommendations stream in dynamically.
  • A news homepage serves its static layout from the CDN edge instantly while live headlines load via dynamic Suspense slots.
  • A checkout page pre-renders the static form shell and streams the user's saved addresses once the session resolves.

More examples

Enable PPR in next.config.ts

PPR is opt-in via the experimental.ppr flag in next.config.ts; enable it before using the feature.

Example · ts
// next.config.ts
import type { NextConfig } from 'next';

export default {
  experimental: {
    ppr: true,
  },
} satisfies NextConfig;

Static shell with dynamic slot

PPR pre-renders everything outside Suspense boundaries and streams the dynamic slots when the server resolves them.

Example · ts
// app/product/[id]/page.tsx
import { Suspense } from 'react';
import Recommendations from './Recommendations';

export const experimental_ppr = true;

export default function ProductPage() {
  return (
    <>
      <h1>Product Detail</h1>   {/* static — prerendered */}
      <Suspense fallback={<p>Loading recs...</p>}>
        <Recommendations />     {/* dynamic — streamed */}
      </Suspense>
    </>
  );
}

Dynamic slot component

Reading cookies() inside a Suspense-wrapped component marks it as dynamic, letting PPR stream it separately.

Example · ts
// app/product/[id]/Recommendations.tsx
import { cookies } from 'next/headers';

export default async function Recommendations() {
  const cookieStore = await cookies();
  const userId = cookieStore.get('user-id')?.value;
  const recs = await fetchRecommendations(userId);
  return <ul>{recs.map((r: any) => <li key={r.id}>{r.name}</li>)}</ul>;
}

Discussion

  • Be the first to comment on this lesson.