With Cache Components, every route is a mix of prerendered and request-time parts; placing them well decides speed.
With Cache Components (on by default in new apps), Next.js renders each route at build time and keeps whatever can complete as a static shell: HTML plus an RSC payload that can be served instantly from a CDN. Parts that need request data are left as holes that stream in at request time. This is called Partial Prerendering.
What can be prerendered:
- static markup, module imports and pure computation,
- results of
'use cache'functions and components (next lessons).
What can't, and must be wrapped in <Suspense> so the rest of the page can still prerender:
- runtime APIs:
cookies(),headers(),searchParams, andparamsthat aren't known at build time, - uncached data: fetches and database queries without
'use cache', - per-request values like
Date.now()orcrypto.randomUUID(), which requireawait connection()first.
If such access isn't inside a Suspense boundary, the dev overlay reports a blocking route and suggests the fix. The deeper in the tree you await, the more of the page lands in the shell: pass the params promise down instead of awaiting it in a layout.
`generateStaticParams` returns the param values to prerender at build time, e.g. your 100 most popular product slugs. Other values render the shell immediately and fill in on demand.
Apps without Cache Components use the previous model, where any runtime API makes the whole route dynamic.
Example
// app/products/[slug]/page.tsx
import { Suspense } from 'react'
import { cookies } from 'next/headers'
import { connection } from 'next/server'
export async function generateStaticParams() {
return [{ slug: 'trail-runner' }, { slug: 'city-sneaker' }] // prerendered at build
}
async function Greeting() {
const name = (await cookies()).get('name')?.value ?? 'guest' // runtime API
return <p>Welcome back, {name}</p>
}
async function RequestId() {
await connection() // opt into request time, then random is fine
return <small>Request {crypto.randomUUID()}</small>
}
export default async function Product({ params }: PageProps<'/products/[slug]'>) {
const { slug } = await params
return (
<main>
<h1>{slug}</h1> {/* in the static shell */}
<Suspense fallback={<p>Hello!</p>}><Greeting /></Suspense>
<Suspense fallback={null}><RequestId /></Suspense>
</main>)
}Remember
- Each route = prerendered static shell + Suspense holes filled at request time
- Runtime APIs, uncached data and per-request values must sit inside
<Suspense> generateStaticParamsprerenders known dynamic params;connection()defers to request time
Watch out for
- Reading
cookies()at the top of a layout, which pushes everything below it out of the static shell.