Partial Prerendering in Next.js: Static Shell + Dynamic Stream
Table of Contents
About the Author

Ahmed Mahmoud
Author & Developer
Software engineer passionate about web development and user experience design.
Founder of Devya · eng-ahmed.com ↗Key takeaways
Partial Prerendering (PPR) in Next.js serves a static HTML shell from the CDN edge instantly, then streams Suspense-wrapped dynamic children from the origin server in the same HTTP response — delivering CDN speed for static content and 100% fresh data for dynamic sections in a single page load.
PPR replaces the ISR vs. dynamic-route tradeoff for pages that are mostly static with isolated personalized or real-time sections. Enabling it requires no changes to how you write Server Components or Suspense — only a config flag and a per-route export.
PPR and use cache (Next.js 15.5+) are complementary: the static shell gets CDN delivery; Server Components inside Suspense boundaries get origin-level memoization via use cache to avoid redundant database fetches.
What does Partial Prerendering actually do?
PPR splits a page into two rendering phases in the same HTTP response. At build time, Next.js freezes everything that does not read dynamic request data into a static HTML shell deployed to the CDN edge. At request time, the CDN delivers the shell at edge latency while the origin streams each Suspense boundary's content into the same response. On a product page, navigation, title, and description arrive at CDN speed; the in-stock badge and personalized recommendations stream from the origin a fraction of a second later.
How is PPR different from ISR, use cache, and streaming Suspense without PPR?
ISR gives CDN speed but makes the whole page potentially stale. Full dynamic rendering keeps all data fresh but every request hits the origin. Streaming Suspense without PPR streams from the origin so TTFB still includes origin latency. PPR combines CDN-edge delivery for the static shell with per-request freshness for each Suspense island — the best characteristics of both approaches.
How do we enable and use PPR in a Next.js app?
PPR requires Next.js 15 or later. Set experimental.ppr to 'incremental' in next.config.ts, then export const experimental_ppr = true from each route that should use it. All existing Server Component and Suspense code continues to work unchanged. Next.js enforces the PPR boundary: if a component outside Suspense reads cookies() or headers(), the build fails with a clear error.
What belongs in the static shell vs. inside Suspense?
Static shell: navigation, header, footer, content that changes rarely, layout structure, images that do not depend on the user's session, skeleton shapes. Inside Suspense boundaries: user-specific content (cart, recommendations), inventory or pricing that changes frequently, A/B test cookie reads, any component that calls cookies(), headers(), or searchParams.
How do PPR and use cache compose?
PPR sets the CDN boundary — static shell to the edge, dynamic Suspense children to the origin. use cache controls which origin renders are memoized. A Suspense child marked with 'use cache' still streams from the origin, but the origin serves it from the component cache after the first render rather than re-fetching from the database on every request. Tag it with cacheTag for surgical invalidation. Both optimizations stack without conflict.
When should we use PPR instead of full dynamic rendering?
Use PPR when the page is mostly static with isolated dynamic sections: a product page with real-time inventory, a blog post with a session-tied reading-progress indicator, a SaaS dashboard with a cached sidebar plus live metrics tiles. If the entire page depends on per-request data, PPR's static shell has nothing to cache and full dynamic rendering is simpler.
FAQ
**Q:** Does PPR require changes to how we write Server Components or Suspense? **A:** No. The only code changes are experimental.ppr: 'incremental' in next.config.ts and export const experimental_ppr = true per route. All existing patterns work unchanged.
**Q:** What happens to SEO with PPR enabled? **A:** The static shell is fully crawlable HTML served from the CDN. Content inside Suspense that streams in afterward may or may not be indexed depending on the crawler. Keep SEO-critical content in the static shell.
**Q:** Can we use PPR and ISR on the same route? **A:** No. A route opts into either PPR or ISR, not both. PPR replaces ISR for that route: the static shell is the build-time snapshot and dynamic Suspense children are always fresh per request.
**Q:** Is PPR stable in Next.js 15 and 16? **A:** PPR uses the incremental opt-in model as of Next.js 15 and remains experimental through Next.js 16. It is production-safe — Vercel uses it on their own properties — but the API may change before graduating to stable.
**Q:** How do we verify PPR is working? **A:** Run next build. Routes with PPR show a split between static and dynamic segments in terminal output. In Chrome DevTools Network, the HTML response arrives in chunks: static shell first, then the Suspense stream.
Further Reading
Frontend Engineering
Modern CSS Replaced Our Layout JavaScript: Container Queries, :has(), and Subgrid in Production
Container queries, :has(), subgrid, and cascade layers let our team delete most of the layout JavaScript we used to ship — ResizeObserver wrappers, parent-class toggles, and height-measuring passes. Here is what each feature replaces, and the two we still treat as progressive enhancement.
Frontend Engineering
AbortController Beyond fetch: Cancellation Patterns We Use in Every React App
Uncancelled fetches do not fail loudly — they resolve late and overwrite fresh state. The AbortController patterns we use in every React app we ship: useEffect cleanup, AbortSignal.timeout(), AbortSignal.any(), and one-signal listener cleanup.