Partial Prerendering combines a static shell with dynamic "holes" that stream in at request time. It's the closest thing Next.js has to a silver bullet.
Visit a homepage:
0 ms — HTML arrives: navbar, hero, layout — all static, cached on the CDN
50 ms — page is interactive; static content paints
50–250 ms — dynamic holes stream in: user badge, cart count, personalized feed
The user sees something instantly. The dynamic bits arrive a moment later — over the same response stream.
Per-route in Next.js 16:
1// app/page.tsx
2export const experimental_ppr = true;Or globally:
1// next.config.ts
2const config: NextConfig = {
3 experimental: { ppr: "incremental" },
4};"incremental" means: only routes that explicitly opt in get PPR. true means: all routes.
The static shell is the page body. Dynamic content goes inside a <Suspense>:
1import { Suspense } from "react";
2
3export const experimental_ppr = true;
4
5export default function HomePage() {
6 return (
7 <main>
8 <Hero /> {/* static */}
9 <FeaturedSection /> {/* static */}
10 <Suspense fallback={<UserBadgeSkeleton />}>
11 <UserBadge /> {/* dynamic — reads cookies */}
12 </Suspense>
13 <Footer /> {/* static */}
14 </main>
15 );
16}When Next.js builds, it pre-renders everything outside <Suspense> to static HTML. At request time, only the UserBadge work happens — its HTML streams into the document via a <template> tag.
Inside a <Suspense> boundary, you can use any dynamic API:
cookies(), headers(), draftMode()searchParams access (via props)fetch(url, { cache: "no-store" })Outside the boundary: stick to static.
1export const experimental_ppr = true;
2
3export default function Home() {
4 return (
5 <>
6 <Hero />
7 <ValueProps />
8 <Suspense fallback={<PricingSkeleton />}>
9 <PersonalizedPricing /> {/* fetches per visitor */}
10 </Suspense>
11 <Testimonials /> {/* static */}
12 <FAQ /> {/* static */}
13 <CTA />
14 </>
15 );
16}The marketing page can still serve from a CDN at the edge — only the pricing fragment hits the origin.
After next build watch for the ◐ (partial) symbol next to your route:
Route (app)
◐ / (static shell + dynamic holes)
Open the page in incognito and watch DevTools' Network tab — you'll see a single HTML response stream with multiple sections delivered in chunks.