# Partial Prerendering Blends Static and Dynamic

Source: https://www.egnworks.com/blog/partial-prerendering-explained  
Author: Jacob Val  
Published: 2026-09-11  
Updated: 2026-09-12  
Category: Frontend Architecture  
Tags: Partial Prerendering, Rendering

> A deep technical guide to Partial Prerendering, covering the cacheComponents flag, the use cache directive, cacheLife profiles and prerender thresholds, cacheTag invalidation, connection() for request time rendering, and how the static shell is assembled.

---

A route in most frameworks has always had to pick one rendering strategy for the entire page. Static generation produces the fastest possible response, served straight from a CDN, but it cannot show anything that depends on the specific request, such as inventory counts or a signed in user's name. Server rendering can show that request specific content, but it means every visitor waits for the server to run before any HTML arrives, even for the parts of the page that never change. A product page with one live inventory count next to an otherwise static description has never fit cleanly into either model.

## Why Static and Dynamic Used to Be an All or Nothing Choice

Static generation renders a page once at build time and serves the same HTML to every visitor until the next rebuild. It is the cheapest possible response, but any content that has to differ per request forces the whole route out of this model, since a statically generated file cannot know what a specific visitor should see.

Server rendering runs on every request instead, which restores the ability to show request specific content, but at a real cost. The server has to finish computing the entire page before sending any of it, so even a header and footer that are identical on every visit end up waiting behind the one dynamic value further down the page.

## What Partial Prerendering Actually Does

Partial Prerendering splits a single route into a static shell, generated once at build time, and one or more dynamic segments that stream in after the shell is already in the browser, within a single response rather than as two separate round trips. The visitor receives the static shell instantly, the same way they would from a purely static page, while the parts of the page that depend on the specific request resolve in the background and stream into place.

## Three Ways a Component Can Render

Under Cache Components, the model that implements Partial Prerendering, what a component does determines which part of the response it ends up in.

| What the component does | What happens during prerendering |
| --- | --- |
| Marked with the `use cache` directive | The result is computed once, cached, and included directly in the static shell, subject to the thresholds described below |
| Wrapped in `<Suspense>` around a runtime read such as cookies, headers, or an uncached fetch | Its fallback ships inside the static shell, and the real content streams in once the request resolves |
| Only synchronous, predictable work such as module imports or pure computation | Completes automatically during prerendering and becomes part of the shell without any directive |

## Enabling It in Next.js

Partial Prerendering is enabled through the `cacheComponents` flag, which also turns on the `use cache` directive and related caching functions.

```ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  cacheComponents: true,
};

export default nextConfig;
```

This is a change from how the feature originally shipped, as an experimental, route level flag called `experimental_ppr`. As of Next.js 16, `cacheComponents` is a single project wide setting that unifies what used to be three separate experimental flags, PPR, `useCache`, and `dynamicIO`, into one.

## Keeping the Static Shell as Large as Possible

Where a dynamic read happens in the component tree determines how much of the page can be prerendered. A layout that reads a request specific value at the top of the tree forces everything below it to wait, even content that has nothing to do with that value.

```tsx
import { Suspense } from "react";

export default function ProductPage(props: { params: Promise<{ slug: string }> }) {
  return (
    <div>
      <SiteHeader />
      <Suspense fallback={<p>Loading product...</p>}>
        <ProductInfo params={props.params} />
      </Suspense>
    </div>
  );
}

async function ProductInfo({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const product = await getProduct(slug);
  return (
    <>
      <h1>{product.name}</h1>
      <p>${product.price}</p>
    </>
  );
}

async function getProduct(slug: string) {
  "use cache";
  return db.products.findBySlug(slug);
}
```

`SiteHeader` has no dependency on the request and becomes part of the static shell automatically. The slug dependent work sits inside its own `Suspense` boundary, so only that part of the page waits while the rest of the shell ships immediately.

## Assigning a Cache Lifetime with cacheLife

A `use cache` scope without an explicit lifetime falls back to a `default` profile, which is difficult to reason about once caches nest inside each other. `cacheLife`, called from within the cached function, assigns one of several built-in profiles, each balancing three timing values.

| Profile | Use case | `stale` | `revalidate` | `expire` |
| --- | --- | --- | --- | --- |
| `default` | Standard content | 5 minutes | 15 minutes | never |
| `seconds` | Real time data such as stock prices | 30 seconds | 1 second | 1 minute |
| `minutes` | Frequently updated content such as a social feed | 5 minutes | 1 minute | 1 hour |
| `hours` | Content updated multiple times a day, such as inventory | 5 minutes | 1 hour | 1 day |
| `days` | Content updated daily, such as a blog post | 5 minutes | 1 day | 1 week |
| `weeks` | Weekly content such as a newsletter | 5 minutes | 1 week | 30 days |
| `max` | Content that rarely changes, such as a legal page | 5 minutes | 30 days | 1 year |

```tsx
import { cacheLife } from "next/cache";

async function getProduct(slug: string) {
  "use cache";
  cacheLife("hours");
  return db.products.findBySlug(slug);
}
```

`stale` governs how long the client router can reuse cached content without checking the server again. `revalidate` sets how often the server regenerates the cached result in the background once a request arrives after that window. `expire` is the outer bound, after which the next request waits for a fresh result synchronously rather than serving a stale one. A project can also redefine the built-in profiles or register its own named profiles in `next.config.ts`, and a one-off case can pass an inline object directly to `cacheLife` instead of a preset name.

## What Determines Whether Cached Content Joins the Static Shell

Not every `use cache` result is eligible for the static shell. The lifetime assigned by `cacheLife` decides that.

| Lifetime condition | Result |
| --- | --- |
| `revalidate` of `0`, or `expire` under 5 minutes | Excluded from prerendering entirely, becoming a dynamic hole resolved at request time |
| `stale` under 30 seconds | Excluded from prerendering, since a prefetch would expire before a visitor could act on it |
| `stale` of at least 30 seconds but under 5 minutes | Included in prerendering, but excluded from the route's App Shell used for client side navigations |
| `stale` of 5 minutes or more | Included in both the static shell and the App Shell |

Of the built-in presets, only `seconds` falls below one of these thresholds, since its one minute `expire` excludes it from prerendering outright. This is also why nesting a short lived cache inside a longer lived one without an explicit lifetime on the outer scope raises an error during prerendering. Next.js will not silently let a one second inner cache turn a fifteen minute outer cache short, so the outer scope has to state its own lifetime explicitly once a short lived cache is nested inside it.

## Invalidating Cached Content with cacheTag

A cache lifetime handles content that goes stale on a schedule. Content that needs to be invalidated the moment it changes, such as a record a user just edited, is handled separately with `cacheTag`.

```tsx
import { cacheTag } from "next/cache";

async function getBookings(type: string) {
  "use cache";
  cacheTag("bookings-data");
  const res = await fetch(`https://api.example.com/bookings?type=${type}`);
  return res.json();
}
```

A Server Function can then purge every cache entry carrying that tag once the underlying data changes.

```tsx
"use server";

import { updateTag } from "next/cache";

export async function submitBooking() {
  await addBooking();
  updateTag("bookings-data");
}
```

`updateTag` clears the tagged entries immediately and is meant for read your own writes cases, such as a form submission where the next read should reflect the change right away. `revalidateTag` does the same invalidation but allows stale data to keep serving while the fresh version regenerates in the background, which fits a webhook or a route handler more than a direct user action.

## Forcing Request Time Rendering with connection()

Some components need a different result on every request without reading a request time API such as `cookies()` or `headers()`, for example a component that calls `Math.random()` or reads the current time. Left alone, values like these would either throw during prerendering or silently bake a single value into the static shell. `connection()` tells the framework to stop prerendering at that point and wait for a real request instead.

```tsx
import { connection } from "next/server";

async function VisitorId() {
  await connection();
  const id = crypto.randomUUID();
  return <span>{id}</span>;
}
```

Everything before the `await connection()` call can still be shared across requests. Everything after it runs fresh for each one, the same way a component wrapped in `Suspense` around a cookie read behaves, just without an actual request time API triggering it.

## What Streams In Instead of Blocking the Whole Page

Reading cookies, headers, or search parameters used to mean the entire route had to render on the server for every request. Under Partial Prerendering, a component that reads one of these can be wrapped in its own `Suspense` boundary, so only that component streams in at request time while everything else, including other dynamic components with their own boundaries, resolves independently. A page can show a personalized greeting derived from a cookie right alongside a cached product description and a live inventory count, each streaming in on its own schedule, without any of them blocking the others or the initial shell.

## What It Does Not Fix

Partial Prerendering changes how a page is delivered, not how much work the underlying data source has to do. A slow database query behind a `use cache` function still takes as long to compute the first time; caching only avoids repeating that work on subsequent requests within its lifetime.

It is also a framework level rendering model rather than a browser standard, so `use cache`, `cacheLife`, `cacheTag`, and `cacheComponents` are particular to Next.js rather than portable to any framework. Search engine crawlers are handled differently as well, since they need a complete document rather than a stream: the framework renders the full page for them synchronously instead of sending a shell and letting content stream in afterward.

## Conclusion

Partial Prerendering removes the assumption that a route has to be entirely static or entirely dynamic. A page can ship a static shell the instant a request arrives while the parts of it that genuinely depend on that request resolve independently behind their own loading states. What began as an experimental, route level flag has since become the default behavior of a caching model with its own vocabulary, `use cache` for what gets computed once, `cacheLife` for how long it stays valid and whether it can join the shell at all, `cacheTag` for invalidating it on demand, and `connection()` for the cases that need to opt out of prerendering entirely.

## References

[Next.js Docs: Caching](https://nextjs.org/docs/app/getting-started/caching)

[Next.js Docs: cacheComponents Configuration](https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents)

[Next.js Docs: cacheLife Function](https://nextjs.org/docs/app/api-reference/functions/cacheLife)

[Next.js Docs: cacheTag Function](https://nextjs.org/docs/app/api-reference/functions/cacheTag)

[Next.js Docs: connection Function](https://nextjs.org/docs/app/api-reference/functions/connection)

[React Docs: Suspense](https://react.dev/reference/react/Suspense)
