#Next.js#React#Web Development#Vercel#TypeScript

Next.js 16 Partial Prerendering and Cache Components: A Practical Guide

webhani·

Next.js 16 formalizes two capabilities that change how routes are rendered: Partial Prerendering (PPR) and Cache Components. Together, they let you colocate static content and dynamic data within a single route, removing the all-or-nothing choice between SSG and SSR that dominated Next.js architecture decisions for years.

The Problem PPR Solves

Before PPR, a route was either fully static (generated at build time) or fully dynamic (evaluated per request). The moment any part of a page needed real-time data, the entire route moved to SSR — including navigation, layout, and content that hadn't changed since the last deploy.

PPR splits a route into two layers:

  • Static shell: built at deploy time, served from the CDN edge with near-zero latency
  • Dynamic regions: evaluated per request, streamed into the shell via Suspense

The shift is architectural. Instead of asking "should this route be SSG or SSR?", the question becomes "what is the freshness requirement of each piece of data on this page?"

Implementation

Basic PPR Structure

// app/products/[id]/page.tsx
 
import { Suspense } from "react";
import { ProductShell } from "./components/ProductShell";
import { DynamicInventory } from "./components/DynamicInventory";
import { DynamicReviews } from "./components/DynamicReviews";
 
// The route prerenders as a static shell
export default function ProductPage({ params }: { params: { id: string } }) {
  return (
    <ProductShell id={params.id}>
      {/* Dynamic regions wrapped in Suspense */}
      <Suspense fallback={<InventorySkeleton />}>
        <DynamicInventory productId={params.id} />
      </Suspense>
      <Suspense fallback={<ReviewsSkeleton />}>
        <DynamicReviews productId={params.id} />
      </Suspense>
    </ProductShell>
  );
}

ProductShell is prerendered to HTML at build time and served from the CDN. DynamicInventory and DynamicReviews are Server Components that stream in at request time. The user sees the shell immediately while dynamic regions load.

Cache Components

Cache Components let you cache the output of specific components inside otherwise dynamic routes. Useful for content that changes on a schedule (promotional banners, featured items) rather than per-request.

// components/FeaturedBanners.tsx
import { unstable_cache as cache } from "next/cache";
 
const getCachedBanners = cache(
  async () => {
    const banners = await db.banners.findMany({ where: { active: true } });
    return banners;
  },
  ["featured-banners"],
  { revalidate: 3600, tags: ["banners"] }
);
 
export async function FeaturedBanners() {
  const banners = await getCachedBanners();
  return (
    <ul>
      {banners.map((banner) => (
        <li key={banner.id}>{banner.title}</li>
      ))}
    </ul>
  );
}

revalidate: 3600 causes the cache to revalidate hourly. The tags field enables on-demand invalidation.

On-Demand Revalidation

// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";
import { NextRequest, NextResponse } from "next/server";
 
export async function POST(request: NextRequest) {
  const { tag, secret } = await request.json();
 
  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
 
  revalidateTag(tag);
  return NextResponse.json({ revalidated: true, tag });
}

Wire this endpoint to a CMS webhook. When an editor publishes new content, the webhook hits /api/revalidate with tag: "banners", and the next request to any cached component with that tag triggers a fresh fetch.

Migrating from Next.js 15

If you enabled PPR experimentally in Next.js 15, the main change in Next.js 16 is that experimental.ppr is no longer required — PPR is on by default.

// next.config.mjs — Next.js 16
const nextConfig = {
  // experimental.ppr removed; PPR is now the default behavior
  // No flag needed unless you're explicitly opting out
};
 
export default nextConfig;

The more significant migration concern is Turbopack becoming the default bundler in Next.js 16. If your project uses custom Webpack loaders or plugins, audit compatibility before upgrading. Turbopack handles the common cases well, but custom build pipelines can have edge cases that require attention.

Where PPR Delivers and Where It Doesn't

Page typeStatic regionsDynamic regionsPPR impact
E-commerce product pageDescription, images, specsInventory, pricing, reviewsHigh — immediate static paint
DashboardNavigation, layoutKPIs, real-time feedsMedium — layout shift reduction
Blog postArticle body, metadataPersonalized recommendationsMedium — TTFB matches static
Fully authenticated appCommon UI shellAll user-specific dataMedium — shell preloading

PPR gives the most benefit when a significant portion of the page is static but one or two regions require dynamic data. Fully dynamic pages (personalized dashboards where everything is user-specific) and fully static pages (marketing landing pages) see limited gains.

Practical Considerations

Map data freshness requirements first. Before restructuring routes around PPR, categorize each data dependency: build-time, time-based (hourly, daily), or per-request. This classification directly maps to the implementation: static shell, Cache Component with revalidate, or dynamic Server Component.

Separate Turbopack migration from PPR adoption. If you have custom Webpack configuration, treat Turbopack migration as a distinct task from the Next.js 16 feature upgrade. Bundler issues and PPR issues are hard to diagnose when introduced simultaneously.

Design Suspense boundaries intentionally. Suspense placement in PPR directly affects user experience. Too coarse and you get visible layout shifts; too granular and you introduce implementation overhead. Including Skeleton components in your design system upfront gives you consistent fallback UI across the codebase.

The direction Next.js 16 formalizes — server-first rendering with fine-grained control over data freshness — is consistent with where SvelteKit and Nuxt are heading as well. Across the frontend ecosystem in 2026, reducing client-side JavaScript and moving computation server-side is increasingly the default, not the exception.


Sources: Next.js Updates by Vercel - June 2026, Next.js 15 and Beyond