We use cookies. Privacy Policy

    guide

    Partial Prerendering: a practical guide for 2026

    By PageGlass Team, SEO Engineering

    Partial Prerendering (PPR) is Next.js's answer to the static-vs-dynamic rendering trade-off. Instead of choosing one mode per route, PPR lets a single page ship a cached static shell from the edge while streaming personalised content into the same response. This guide explains how it works, when it shipped to stable, the trade-offs, and what to do if you can't or don't want to migrate to the Next.js App Router.

    Partial Prerendering renders a static HTML shell at build time and streams dynamic, per-request content into the same HTTP response. It became stable in Next.js 16 (October 2025) as part of Cache Components, an opt-in model enabled via cacheComponents: true. PPR is App Router only; SPAs on other frameworks need a different approach.

    What is Partial Prerendering?

    Partial Prerendering is a Next.js rendering strategy that splits each page into a static shell prerendered at build time and dynamic holes streamed at request time. The shell ships from the CDN immediately; the dynamic content streams into the same response moments later, with no client-side fetch.

    Most rendering models force a route-level decision: either the whole page is static (SSG), the whole page is server-rendered on every request (SSR), or it's a cached SSR variant (ISR). PPR replaces that page-level dichotomy with a component-level one. Anything wrapped in a <Suspense> boundary, or that reads a request-time API like cookies(), headers(), or the searchParams prop, becomes a dynamic hole. Everything else - the layout, the headings, the marketing copy, any data marked with 'use cache' - becomes part of the prerendered shell.

    The result: a product page can prerender the photo, title, description, and structured data at build time, while the cart count, recommended items, and personalised greeting stream in as the request resolves. Visitors see the layout and primary content instantly; bots see all of it in a single HTML response.

    How PPR works under the hood

    At build time, Next.js generates two artefacts per PPR route: a static HTML shell containing Suspense fallbacks, and a serialised "postponedState" blob recording where rendering paused. At request time, the shell streams from the edge while the server resumes rendering the dynamic boundaries into the same HTTP response.

    Internally, Next.js's prerender pipeline pauses at Suspense boundaries and at request-time API call sites. It captures the partial HTML and a postponed state value to the build output. At runtime, that postponed state is what the server resumes from to render only the dynamic boundaries.

    At runtime, the request lifecycle looks like this:

    1. When the shell is cached at the edge, the CDN serves it immediately and TTFB drops to edge latency. On a cold cache or a region without the shell, the response falls back to a regular SSR round-trip.
    2. The server resumes rendering using the postponed state, executing only the components inside dynamic boundaries.
    3. As each Suspense boundary resolves, its rendered HTML is streamed into the same response using HTTP chunked transfer encoding. From the browser's perspective there is no second request - the dynamic content arrives as additional chunks of the same HTML response, not via a follow-up fetch.
    4. The browser reveals each boundary as its HTML chunk arrives, replacing the Suspense fallback with the resolved content. React 18+ hydrates progressively, attaching event handlers to each boundary as its chunk arrives, rather than waiting for the whole document.

    For platforms running a CDN-edge + origin-compute split, Next.js exposes adapter hooks so the CDN can forward the postponed state to an origin function and concatenate the streamed result with the cached shell. The exact wire format is implementation-specific; Vercel runs the canonical version on its own infrastructure.

    Status in 2026: stable in Next.js 16

    PPR is stable as of Next.js 16, released October 2025. The previous experimental.ppr flag and experimental_ppr route export are replaced by Cache Components, opt-in via cacheComponents: true. Routes that mix cached and uncached segments are automatically partially prerendered; routes that don't stay fully static or fully dynamic.

    The timeline:

    • October 2023 - Announced as experimental at Next.js Conf.
    • Next.js 14 (2023-2024) - First implementation behind the experimental.ppr flag, with the 'incremental' mode and experimental_ppr route-level export both available during the 14.x cycle.
    • Next.js 15 (October 2024) - PPR continued under the same experimental flags. Vercel still positioned PPR as not yet stable; the 'incremental' opt-in remained the recommended way to try it on a per-route basis.
    • Next.js 16 (October 2025) - PPR graduates to stable as part of the Cache Components feature. The previous experimental flags are replaced by the new cacheComponents opt-in.

    If you're reading documentation that warns "PPR is experimental, do not use in production," check the publish date. The "experimental" framing from before October 2025 is now outdated, though older articles about the underlying mechanics often remain accurate. The current Next.js docs treat PPR as part of Cache Components rather than a standalone feature flag.

    How to enable PPR in your Next.js app

    On Next.js 16+, enable PPR by setting cacheComponents: true in next.config.ts. Components that read cookies(), headers(), searchParams, or call connection() must be wrapped in a Suspense boundary. The build raises "Uncached data was accessed outside of <Suspense>" when this rule is broken.

    Minimal config:

    // next.config.ts
    import type { NextConfig } from 'next'
    
    const nextConfig: NextConfig = {
      cacheComponents: true,
    }
    
    export default nextConfig

    A typical PPR page mixes static and dynamic content like this:

    // app/dashboard/page.tsx
    import { Suspense } from 'react'
    import { cookies } from 'next/headers'
    
    async function UserGreeting() {
      const theme = (await cookies()).get('theme')?.value
      return <p>Hello, your theme is {theme}</p>
    }
    
    export default function Page() {
      return (
        <>
          <h1>Dashboard</h1>            {/* static shell */}
          <Suspense fallback={<p>Loading...</p>}>
            <UserGreeting />             {/* dynamic hole */}
          </Suspense>
        </>
      )
    }

    Cache Components flips the implicit caching model. Previously, fetched data was cached by default unless opted out. Now, nothing is cached unless you explicitly mark it with 'use cache'. A Suspense boundary is a separate mechanism: it doesn't cache anything, but it tells Next.js the boundary is allowed to render at request time and stream into the response. Cached and request-time access aren't interchangeable - request-time APIs like cookies(), headers(), and connection() can't be called inside a 'use cache' scope, because cached output by definition can't vary per request. The build error Uncached data was accessed outside of <Suspense> is the most common gotcha on day one.

    For Next.js 15, the legacy API still works on existing apps:

    // next.config.ts
    const nextConfig = { experimental: { ppr: 'incremental' } }
    
    // app/some-route/page.tsx
    export const experimental_ppr = true

    PPR vs SSR, SSG, and ISR

    PPR overlaps with SSG, SSR, and ISR rather than replacing them outright. It's most useful when a page mixes shared cacheable content with per-request dynamic content in the same route. Pure SSG, SSR, and ISR each retain niches where they're still the better fit, and PPR can be combined with all three depending on which pieces of the page are cached.

    The core Next.js rendering modes compared:

    • SSG generates HTML at build time and serves it to everyone with no origin compute on each request. PPR adds the ability to stream dynamic data into the same response. Pure SSG still has cost and simplicity advantages when there's no per-request data at all.
    • SSR renders the entire page on every request. Useful for fully personalised pages with no cacheable content. Where there's meaningful shared layout or cacheable data, PPR can serve that shell from the edge while only the dynamic boundaries hit your origin.
    • ISR caches a fully-rendered page for a TTL and revalidates in the background. Useful for shared content like catalogue pages and news feeds. PPR with 'use cache' and cacheLife covers many ISR use cases and adds per-request dynamic boundaries inside the same response.

    If you're not on Next.js, none of these are directly available. A separate category - dynamic rendering services - sits in front of any framework and serves pre-rendered HTML to bot user-agents while humans get the SPA. These solve the SEO problem (giving crawlers fully rendered HTML) without a framework migration, but they don't reduce TTFB for human visitors the way PPR does.

    The decision: if you're on the Next.js App Router and willing to adopt Cache Components, PPR is the natural fit for most use cases. If you're on a React, Vue, or Angular SPA - or on Next.js Pages Router - PPR isn't available, and the alternatives are framework migration, static export, or dynamic rendering. Our JavaScript SEO rendering guide covers the trade-offs across these paths.

    What PPR means for SEO

    Crawlers receive fully rendered HTML for the static shell immediately, with dynamic boundaries streamed into the same response. The shell content can be indexed without waiting for JavaScript execution; streamed content is also visible to crawlers that wait for the response to complete.

    The SEO upsides:

    • TTFB drops to edge latency for the static shell. Google adjusts crawl rate in response to server response speed, so faster TTFB tends to support more efficient crawl budget usage on large sites.
    • LCP can improve when hero content lives in the shell, because the shell is served from the edge before any dynamic data resolves. Real-world impact depends on what proportion of above-the-fold content is in the shell vs the dynamic boundaries.
    • No two-pass indexing required for shell content. Googlebot's two-phase pipeline processes JavaScript on a separate render queue, which Google has said usually completes within minutes but can take longer for low-priority pages. PPR sidesteps the queue for everything in the static shell.
    • Predictable HTML structure. The shell contains layout, navigation, headings, structured data, and hero content - exactly what you want crawled.

    Watch for the footguns:

    • If your <h1> or primary copy lives inside a Suspense boundary, you've moved it out of the prerendered shell. The streamed version still arrives in the same response, but you've given up the TTFB win for that content. Put dynamic holes around personalised, non-SEO-critical surface area like cart and recommendations.
    • generateMetadata can become a request-time bottleneck if it reads cookies, headers, or other dynamic data. Next.js 15.2+ supports streaming metadata so a slow generateMetadata doesn't always block the shell, but cached or fully static metadata still loads earlier and is the safer default. Keep title, description, canonical, and OG tags static or cached where possible.
    • Where you place Suspense fallbacks matters. A wide Suspense boundary high in the tree (for example, wrapping the whole layout) effectively pulls the entire subtree out of the static shell, which removes most of the PPR benefit for that route.
    • PPR doesn't fix non-Next.js apps. A React SPA on Vite, an Angular app, or a Next.js Pages Router project still needs SSR, SSG, or a dynamic rendering service to give crawlers visible content. Our SEO audit tool shows the rendering gap on any URL.

    When PPR isn't the right choice

    PPR is App Router only. Non-Next.js stacks can't adopt it; Pages Router teams can't either without migrating. Teams already shipping a stable app may not want the Cache Components migration cost. In those cases, the realistic alternatives are migrating to an SSR-first framework, generating a static export, or adding a dynamic rendering layer.

    PPR is a good fit for greenfield Next.js App Router projects and for teams on Next.js 14+ who are willing to migrate to Cache Components. It's not available, or not pragmatic, when:

    • You're on a non-Next.js stack (React with Vite, Vue, Angular, Svelte, Solid, Qwik).
    • You're on Next.js Pages Router and don't have capacity to migrate to App Router.
    • You're building on no-code platforms (Lovable, Bolt, Replit, Webflow, Bubble) where you don't control the rendering stack. (AI-builder tools that emit Next.js code, like v0, are different - you typically can adopt PPR there because you own the generated code.)
    • You manage many client sites on different frameworks and need a single solution that works across all of them.

    The three realistic alternatives in these cases:

    • Migrate to an SSR-first framework - Next.js App Router, Nuxt, SvelteKit, Remix, or Astro with SSR. Solves SEO natively. The migration itself is the cost.
    • Generate a static export - viable when content is largely fixed and personalisation is minimal. Doesn't suit dashboards, real-time data, or per-user views.
    • Add a dynamic rendering layer - sits in front of the existing app and serves pre-rendered HTML to bot user-agents while humans hit your origin unchanged. PageGlass implements this at the DNS layer with a CNAME record.

    Which path is right depends on team capacity, existing stack, and whether the SEO gap is the only driver. PPR is generally the better fit for greenfield Next.js work; the alternatives above apply when a Next.js migration isn't on the cards.

    Frequently asked questions

    Is Partial Prerendering the same as streaming with Suspense?

    No. Streaming SSR renders the entire page on every request and streams chunks as they're ready - the whole page is dynamic. PPR prerenders a static shell at build time, which the CDN can cache and serve from the edge, and only streams the dynamic Suspense boundaries at request time. The shell is trivially reusable across visitors; streaming SSR responses can be cached too, but the whole page revalidates together rather than splitting cached and dynamic regions per component. <a href="/blog/javascript-seo-rendering-guide">Our rendering guide</a> covers the full set of strategies.

    Does PPR fetch dynamic content via AJAX?

    No. The shell and dynamic content are streamed in the same HTTP response using chunked transfer encoding. There's no second request and no client-side fetch for the dynamic content. React hydrates each Suspense boundary in place as its content arrives in the stream.

    Is Partial Prerendering experimental?

    Not anymore. PPR became stable in Next.js 16, released October 2025, as part of the Cache Components feature. The experimental.ppr config flag and the experimental_ppr route-level export have been removed. If you read a guide saying "don't use in production," check the publish date - anything pre-October 2025 is outdated.

    Can I use PPR outside of Next.js?

    No. PPR depends on React's postpone primitive plus Next.js's build pipeline and adapter resume mechanism. Other frameworks address overlapping problems with different mechanics - Astro Islands ship near-zero JS by default, Qwik resumability skips hydration, Remix supports streamed deferred loaders. None are drop-in replacements for PPR's specific build-time-shell + request-time-stream model. Non-Next.js apps that need crawler-visible HTML typically use SSR or a dynamic rendering service.

    Does PPR replace dynamic rendering services like Prerender.io or PageGlass?

    Only if you're on the Next.js App Router and willing to migrate to Cache Components. Dynamic rendering services solve the same SEO problem (giving crawlers fully rendered HTML) for the larger universe of SPAs running on Vite, Vue, Angular, Svelte, no-code platforms, and older Next.js versions. The decision is mostly framework-dependent rather than competitive: PPR is the native answer for new Next.js work, and dynamic rendering is one of several answers for everything else. <a href="/blog/pageglass-vs-prerender-io">PageGlass vs Prerender.io</a> covers the sub-options if dynamic rendering is the route you're on.

    Does PPR help with crawl budget?

    Indirectly, yes. The static shell ships from the CDN at edge latency, which means lower TTFB for crawler requests as well as human ones. Google has stated that crawl rate responds to server response speed and error rate, so consistently faster TTFB tends to support more efficient crawl budget usage on large sites.

    Why does my Next.js build show a route as fully dynamic instead of partially prerendered?

    The most common cause is a request-time API being read outside a Suspense boundary, usually in the layout or page top-level. Triggers include cookies(), headers(), the searchParams or params props (both are Promises in Next.js 15+), and connection() (which awaits the incoming request and opts the calling component out of prerendering; you call it before non-deterministic work like Math.random or crypto.randomUUID). Move the access inside a Suspense-wrapped child component to recover the partial prerender. Other possible causes: dynamic route configs, middleware that adds dynamic headers, or having no 'use cache'-marked segments at all. The build output uses distinct symbols per route type; check next build --debug or current Next.js docs for the exact glyphs in your version.

    Where should I put dynamic content for the best SEO?

    Keep SEO-critical content - h1, primary copy, structured data, meta tags - inside the static shell. Reserve Suspense boundaries for personalised, non-SEO content like cart counts, recommendations, and per-user greetings. Google still receives the full HTML thanks to streaming, but you give up the TTFB win for anything inside a dynamic hole.

    Related articles

    Not on Next.js? Make crawlers see your SPA in minutes

    PageGlass runs at the DNS layer for most SPA frameworks - React, Vue, Angular, Svelte, no-code builders, older Next.js. A CNAME record routes bot traffic through a renderer that serves rendered HTML to search engines, AI crawlers, and social bots, while humans keep the unchanged SPA. 7-day free trial, no card required. PPR is still the better fit for greenfield Next.js work.