Partial Prerendering in Next.js is less about pre-generating pages and more about selectively deferring the rendering of specific components until runtime.
Let’s see this in action. Imagine an e-commerce product page. The product details (name, price, description) are static and can be rendered at build time. However, the "Add to Cart" button’s state (is it disabled if out of stock?) or a real-time stock count needs to be dynamic. With PPR, we can mark the dynamic parts of the page to be rendered on the server after the initial HTML is served.
Here’s a simplified example of how you might mark a component for deferral in app/page.tsx:
import { Suspense } from 'react';
import ProductDetails from './ProductDetails';
import RealTimeStock from './RealTimeStock';
export default function ProductPage({ params }: { params: { id: string } }) {
return (
<div>
<ProductDetails productId={params.id} />
<Suspense fallback={<div>Loading stock...</div>}>
<RealTimeStock productId={params.id} />
</Suspense>
</div>
);
}
In this scenario, ProductDetails would be part of the prerendered HTML. RealTimeStock is wrapped in <Suspense>, which is the key mechanism for deferring its rendering. When the initial HTML for ProductPage is served, the server knows that RealTimeStock needs to be rendered later. It sends down the static HTML for ProductDetails and a placeholder for RealTimeStock. As soon as the client receives this, it makes a request back to the server for the specific data needed by RealTimeStock. The server then renders just RealTimeStock and sends back the HTML snippet, which is injected into the page without a full page reload.
The problem PPR solves is the trade-off between static generation performance and dynamic data needs. Traditionally, you’d either have a fully static page (fast, but stale data) or a fully dynamic page (always fresh, but slower to serve). PPR allows you to get the best of both worlds by prerendering the static parts and deferring the dynamic parts.
Internally, Next.js uses a technique called "streaming HTML." When a page is rendered with deferred components, the server doesn’t wait for all dynamic data to be ready before sending the initial HTML. Instead, it sends the static parts immediately. The deferred components are then rendered on the server and streamed to the client as they become ready, often within <template> tags that the client-side JavaScript later hydrates. This is why the <Suspense> boundary is crucial – it tells Next.js where to insert these streamed chunks of HTML.
The primary lever you control is identifying which components are truly dynamic and can be wrapped in <Suspense>. This isn’t just about fetching data; it’s about any part of the UI that depends on data that might not be available at build time or changes frequently. Think user-specific content, real-time updates, or components that rely on server-side logic that can’t be executed statically. You also need to ensure your data fetching within these deferred components is set up correctly, often using async/await within React Server Components.
The server doesn’t actually generate two separate HTML files for a PPR page. Instead, it generates a single HTML document that contains placeholders for the deferred content. When the client requests these deferred chunks, the server executes the relevant Server Component code, renders it, and sends back just that piece of UI. This avoids the overhead of re-rendering the entire page and only fetches and renders what’s necessary.
The next concept you’ll encounter is managing the loading states and error boundaries for these deferred components more granularly.