DEV Community

Nainik Mehta
Nainik Mehta

Posted on

Next.js Partial Prerendering: A Senior Engineer's Guide

The End of the "Loading Spinner" Era

For years, web performance engineers have been locked in a zero-sum game: the "Static vs. Dynamic" trade-off.

To achieve instant load times, we leaned into Static Site Generation (SSG). But the moment a user logged in, or the moment we needed real-time data, that static foundation crumbled. We were forced into Server-Side Rendering (SSR) or client-side fetching, which introduced the dreaded "Loading Spinner" era. We’ve all been there: a user clicks a link, and they are greeted by a blank screen, followed by a cluster of flickering skeletons or spinners. It feels disjointed. It feels slow.

In 2026, we have a new tool that effectively ends this compromise: Next.js Partial Prerendering (PPR).

What is Partial Prerendering?

Think of your page like a restaurant.

The Old Way (Dynamic Rendering): You order a steak, and the waiter makes you wait outside until the entire meal—sides, drinks, and all—is plated. You get nothing until everything is ready.

The PPR Way: You sit down, and the bread, water, and silverware are already on the table the moment you arrive. The steak follows a few minutes later.

In engineering terms, PPR allows you to pre-render the static shell of a page at build time while keeping dynamic "holes" open for live data. When a user requests the page, the CDN serves the static shell instantly (the "bread and water"), and the dynamic components stream in as they resolve (the "steak").

How It Works Under the Hood

PPR isn't just a clever UI trick; it’s a fundamental shift in how Next.js handles route rendering. It works by splitting a page into two distinct phases:

  1. Build-Time (Static Shell): Next.js renders the route, but when it encounters a Suspense boundary, it pauses. It captures the static HTML of everything outside that boundary and stores it as a "shell."
  2. Request-Time (Streaming Dynamic Parts): When a user hits the page, the CDN serves that cached static shell immediately. Simultaneously, the server resumes rendering the dynamic components inside those Suspense boundaries, streaming the results into the same HTTP response.

Because this happens in a single request using HTTP chunked transfer encoding, the browser starts painting the page layout immediately. There is no second round-trip to the server, and no client-side fetch required for the dynamic parts.

Implementing PPR: A Practical Example

In modern Next.js (16+), PPR is part of the cacheComponents model. To adopt it, you simply need to identify your dynamic boundaries.

Here is how you would structure a product page that has static content (hero image, description) and dynamic content (personalized stock levels):

import { Suspense } from 'react';
import { ProductHero } from '@/components/ProductHero';
import { StockLevel } from '@/components/StockLevel';

// 1. The page is now partially prerendered because 
// we have a Suspense boundary wrapping dynamic data.
export default async function ProductPage({ params }) {
  const product = await getProduct(params.id);

  return (
    <main>
      <ProductHero product={product} />

      {/* 2. This is our dynamic "hole" */}
      <Suspense fallback={<div className="h-10 w-32 bg-gray-200 animate-pulse" />}>
        <StockLevel productId={product.id} />
      </Suspense>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the StockLevel component, you might read from cookies() or perform an uncached fetch(). Because it is wrapped in Suspense, this component becomes a dynamic "island," while the rest of the page remains part of the lightning-fast static shell.

The Architectural Mindset Shift

The hardest part of PPR isn't the code—it's the architectural discipline. You have to decide where the "holes" go.

If you make your boundaries too coarse, you lose the benefits of the static shell. If you make them too fine, you risk a "flicker-fest" of skeletons popping in at different times. The ideal pattern is to place one Suspense boundary per independent dynamic data source, as low in the component tree as possible.

Is PPR a Silver Bullet?

While PPR is a massive leap forward, it’s not a magic fix for slow database queries. If your dynamic component takes three seconds to resolve, PPR will give you a beautiful static shell in 50ms, but the user will still wait three seconds for the dynamic data.

PPR is a perceived-performance tool. It makes your app feel instantaneous by prioritizing the layout and shared content, ensuring the user is never staring at a blank screen.

Are you still relying on global loading states, or have you started architecting your pages with "holes" for dynamic data? The era of the loading spinner is coming to an end. It's time to build for the edge.

Top comments (0)