DEV Community

Nainik Mehta
Nainik Mehta

Posted on

Mastering Next.js Partial Prerendering (PPR) for Performance

The End of the Rendering Tug-of-War

For the better part of a decade, web developers have been trapped in a binary choice. We had to pick between Static (fast TTFB, but stale content) or Dynamic (fresh content, but slow TTFB). If you wanted that instant, edge-cached speed, you sacrificed personalization. If you needed real-time data—like a user’s shopping cart or a live stock ticker—you were forced to opt into full-page dynamic rendering, holding your entire page hostage while the server waited for a database query.

In 2026, that era is officially behind us. With the stabilization of Partial Prerendering (PPR) in Next.js 16, we no longer choose between performance and freshness. We get both.

What is Partial Prerendering?

PPR is a rendering strategy that allows a single page to be split into two distinct parts:

  1. The Static Shell: Everything that doesn't depend on request-time data (layout, navigation, hero images, static text) is prerendered at build time and cached at the CDN edge. This ships to the user in milliseconds.
  2. The Dynamic Holes: Components that require request-time data (user-specific greetings, live inventory, personalized recommendations) are wrapped in React Suspense boundaries. These are rendered on the server at request time and streamed into the existing HTTP response.

The result? The user sees a fully-formed page instantly. There are no global loading spinners, and no waiting for the slowest database query to finish before the browser can even start parsing the HTML.

The Mental Model Shift

The biggest change with PPR is that you no longer define rendering modes at the page level. Instead, you define them at the component level.

Everything in your page is static by default. The moment you introduce a dynamic API—like cookies(), headers(), or an uncached fetch()—you are opting into dynamic rendering for that specific segment. By wrapping those segments in Suspense, you "contain" the dynamic-ness, preventing it from poisoning the rest of your page’s static shell.

A Practical Example

Consider a product detail page. The product description and images are static. The "Add to Cart" button, however, needs to check the user's auth status and the current inventory.

// app/product/[id]/page.tsx
import { Suspense } from 'react';
import { ProductInfo } from '@/components/product-info';
import { LiveInventory } from '@/components/live-inventory';
import { InventorySkeleton } from '@/components/skeletons';

export default async function ProductPage({ params }) {
  const { id } = await params;

  return (
    <main>
      {/* This renders instantly from the edge */}
      <ProductInfo id={id} />

      {/* This streams in after the shell arrives */}
      <Suspense fallback={<InventorySkeleton />}>
        <LiveInventory id={id} />
      </Suspense>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this architecture, the ProductInfo component is part of the static shell. The LiveInventory component is a dynamic hole. When a user visits the page, the CDN serves the shell immediately. The server then computes the LiveInventory and streams it into the page as soon as the data is ready.

Why This Matters for Performance

  1. Instant TTFB: Because the shell is cached at the edge, the Time to First Byte is limited only by the distance to the nearest CDN node, not your origin server's database speed.
  2. No Layout Shift: By using accurate skeleton fallbacks that match the dimensions of your final content, you eliminate Cumulative Layout Shift (CLS). The user sees a stable, albeit partial, page immediately.
  3. Parallel Streaming: You can have multiple Suspense boundaries on a single page, each streaming in independently. If one dynamic component is slow, it doesn't block the others from appearing.

How to Adopt PPR Today

PPR is no longer an experimental flag; it’s a core feature of Next.js 16. To start using it, enable it in your configuration:

  1. Enable Cache Components: Set cacheComponents: true in your next.config.ts.
  2. Audit Your Routes: Use next build to see which routes are static, dynamic, or partially prerendered.
  3. Identify Boundaries: Look for components using cookies(), headers(), or searchParams. Move these into leaf components and wrap them in Suspense.
  4. Use 'use cache': For data that should be cached but isn't automatically, use the 'use cache' directive to ensure it becomes part of the static shell.

The Verdict

If you are still relying on global loading states or forcing entire pages to be dynamic, you are building for the past. PPR is the new standard for modern web architecture. It allows us to build complex, personalized applications without sacrificing the speed that users demand.

The web is no longer a collection of static files or slow-moving dynamic pages. It is a hybrid organism that responds as fast as the edge allows. Are you ready to make the switch?

Top comments (0)