Home/Learn/Next.js/Partial Prerendering

Partial Prerendering

Advanced
Data & Caching

One route that is static and dynamic at once: the shell comes from the CDN instantly, and the personalised holes stream in. It removes the choice this whole category has been about.

Overview

Every concept so far has framed static and dynamic as a per-route decision, with awkward workarounds when a mostly-static page needs one personalised element. Partial Prerendering dissolves that. The static shell is generated at build and served from the edge immediately; anything inside a Suspense boundary that reads request data is streamed in afterwards from the server. It is still experimental at the time of writing, so it belongs in your vocabulary — interviewers ask about it — more than in production today.

The Idea

One request, two phases.

Static shell from the CDN, dynamic holes streamed
// next.config.js
module.exports = { experimental: { ppr: 'incremental' } }

// app/problems/[slug]/page.tsx
export const experimental_ppr = true

export default async function Page({ params }) {
  const problem = await getProblem(params.slug)     // static: build time

  return (
    <>
      <ProblemHeader problem={problem} />           {/* prerendered shell */}
      <ProblemBody problem={problem} />             {/* prerendered shell */}

      <Suspense fallback={<BookmarkSkeleton />}>
        <BookmarkState slug={params.slug} />        {/* reads cookies() —
                                                        streamed per request */}
      </Suspense>
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments slug={params.slug} />             {/* live data */}
      </Suspense>
    </>
  )
}

// What the user gets:
//   ~0ms   the shell, from the CDN — content is readable immediately
//   ~200ms the personalised holes fill in
// Before PPR this page had to be entirely dynamic because of one
// cookie read.

The Rules

What determines which side of the line a component lands on.

Dynamic reads must sit inside a boundary
// Anything reading a dynamic API MUST be inside a Suspense boundary,
// or the whole route falls back to dynamic — the build tells you:
//   "Route / couldn't be partially prerendered because it used
//    cookies outside a Suspense boundary"

// So the discipline is: wrap every dynamic read at the smallest
// possible level, and make the fallback something the shell can show
// with confidence.

// The fallback IS part of the static shell, so it must not itself
// read request data:
<Suspense fallback={<span>Loading…</span>}>    // fine
<Suspense fallback={<UserName />}>             // not fine

// Design implication worth internalising: the shell should be
// meaningful on its own. A page whose static part is only skeletons
// gains nothing from PPR. Put the article, the product, the concept
// in the shell — and only the "is this bookmarked" in the hole.

// This is the same skill as the Suspense concept, applied with a
// different goal: there you were hiding latency, here you are
// deciding what can be cached at the edge.

Where It Stands

Honest status, and what to say when asked about it.

Experimental — adopt the discipline, not the flag
// Experimental at the time of writing. It needs the flag, it is
// tied to recent Next versions, and self-hosting support lags behind
// Vercel. Do not put it under a deadline.

// What is worth saying in an interview: PPR removes the per-route
// static-or-dynamic choice by making the boundary per-COMPONENT
// instead. The shell is cacheable at the edge; only the parts that
// genuinely depend on the request are computed per request.

// The useful part, available today with no flag: the discipline it
// asks for. Wrapping dynamic reads in small Suspense boundaries makes
// a route faster under plain streaming too, and it means the route is
// already PPR-shaped when the feature stabilises.

// Compare with the alternatives you would otherwise reach for:
//   whole route dynamic   simple, slowest, most expensive
//   client-side fetch     static shell, but a spinner and a round trip
//   PPR                   static shell AND no client round trip

// This codebase runs Next 14.2, so PPR is not available here yet —
// which is exactly why the isolate-the-dynamic-part pattern from the
// previous concept is the one to use now.

Key Points to Remember

  • 1PPR serves a prerendered shell from the edge and streams the request-dependent parts into it
  • 2It moves the static-versus-dynamic decision from the route level to the component level
  • 3Any dynamic read must sit inside a Suspense boundary, and the fallback is part of the static shell
  • 4The shell should carry the real content — a shell made only of skeletons gains nothing
  • 5It is still experimental, but the small-boundary discipline it requires improves streaming today

Interview Questions

Sign in to ask Aria
1

What problem does Partial Prerendering solve?

Hard
2

Why must a dynamic read be inside a Suspense boundary for PPR to work?

Hard
3

What can you adopt from PPR today without enabling it?

Medium

Ask Aria about Partial Prerendering

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…