Skip to content
 

Partial Prerendering State

For Partial Prerendering (PPR) routes, Vercel records a PPR state that describes how the page was served for a request. A PPR response is a cached static shell with dynamic holes that are postponed during prerendering, then resumed and streamed in per request. The PPR state tells you which parts of that model actually applied. It shows whether the whole page came from the static shell, part of it was resumed dynamically, or all of it was rendered dynamically.

Every PPR request resolves to one of these states:

StateWhat it means
StaticThe whole page was prerendered. It was served entirely from the static shell, with no dynamic holes to resume.
PartialA static shell was served, and the postponed dynamic parts were resumed and streamed from your function.
DynamicThe static shell was empty, so the entire response came from postponed content resumed by your function.

The page was fully prerendered, so the entire response came from the static shell and there were no postponed parts to resume. Nothing needed to be rendered dynamically for this request, so your function was not invoked.

This is the fastest PPR outcome: the whole page is served from cached static content. A route lands here when it has no dynamic data, or when all of its data is cacheable, so it can be prerendered into the static shell at build time or through later regeneration.

The cache status observed for a Static request can be HIT, MISS, PRERENDER, STALE, or REVALIDATED.

For example, a page whose data is all cacheable prerenders into a full static shell:

app/page.tsx
type Post = { id: number; title: string };
 
async function Posts() {
  'use cache';
  // Cached: prerendered into the static shell
  const posts: Post[] = await fetch('https://api.vercel.app/blog').then((res) =>
    res.json(),
  );
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}
 
export default function Page() {
  return <Posts />;
}
A Static request, served entirely from the static shell.
A Static request, served entirely from the static shell.
A Static request, served entirely from the static shell.
A Static request, served entirely from the static shell.

A static shell was served immediately, and the page's postponed dynamic parts were resumed and streamed in from your function. This is the core Partial Prerendering behavior: the visitor gets the cached shell right away, while the dynamic holes are filled per request.

A Partial state means the page has both a meaningful static shell and dynamic content, such as a cached layout around a personalized or request-specific section.

The cache status observed for a Partial request can be HIT, MISS, PRERENDER, STALE, or REVALIDATED.

For example, a cached list of posts with a personalized greeting resumed per request:

app/page.tsx
import { Suspense } from 'react';
import { cookies } from 'next/headers';
 
type Post = { id: number; title: string };
 
async function Posts() {
  'use cache';
  const posts: Post[] = await fetch('https://api.vercel.app/blog').then((res) =>
    res.json(),
  );
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}
 
async function Greeting() {
  const user = (await cookies()).get('user')?.value;
  return <p>Welcome back, {user}</p>;
}
 
export default function Page() {
  return (
    <main>
      {/* Cached: prerendered into the static shell */}
      <Posts />
      {/* Dynamic: streamed in per request */}
      <Suspense fallback={<p>Loading…</p>}>
        <Greeting />
      </Suspense>
    </main>
  );
}
A Partial request, where the static shell is served and the dynamic parts stream from your function.
A Partial request, where the static shell is served and the dynamic parts stream from your function.
A Partial request, where the static shell is served and the dynamic parts stream from your function.
A Partial request, where the static shell is served and the dynamic parts stream from your function.

The static shell was empty, so the entire response came from postponed content resumed by your function. There was no static content to serve ahead of the dynamic render.

Since the page is fully dynamic, the cache status for a Dynamic request can only be MISS.

For example, an empty <Suspense> boundary above <body> in the root layout opts the route out of a static shell, so nothing below the boundary is prerendered:

app/layout.tsx
import { Suspense } from 'react';
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      {/* Empty fallback above <body>: nothing below this boundary is
          prerendered, so the route's static shell is empty */}
      <Suspense fallback={null}>
        <body>{children}</body>
      </Suspense>
    </html>
  );
}
app/inbox/page.tsx
import { cookies } from 'next/headers';
 
type Message = { id: string; subject: string };
 
export default async function InboxPage() {
  // Per-request data: nothing on this page can be prerendered
  const token = (await cookies()).get('session-token')?.value;
  const messages: Message[] = await fetch('https://api.example.com/inbox', {
    headers: { Authorization: `Bearer ${token}` },
  }).then((res) => res.json());
 
  return (
    <ul>
      {messages.map((message) => (
        <li key={message.id}>{message.subject}</li>
      ))}
    </ul>
  );
}

With an empty shell, there is nothing for the CDN to serve ahead of the render, so the visitor waits on your function for the first byte. A route that consistently shows Dynamic is the signal to move whatever can be cached back above the boundary so a shell exists.

A Dynamic request, rendered entirely by your function.
A Dynamic request, rendered entirely by your function.
A Dynamic request, rendered entirely by your function.
A Dynamic request, rendered entirely by your function.
Last updated August 4, 2026

Was this helpful?