generateStaticParams
Prerender a dynamic route's popular pages with generateStaticParams, render the rest on their first visit, and handle unknown params with Cache Components.
- 01List params for the buildWrite generateStaticParams for one dynamic segment or several, and read the route table and the files the build wrote.
- 02Know what happens to the restTell what the first and the second visit to an unlisted param get, and why dynamicParams doesn't work with Cache Components.
- 03Give the build what it needsReturn at least one param, wait for params inside Suspense, and choose which pages to make ahead.
Two teas made before anyone asks
Dynamic routes gave the tea shop one page for every tea, in app/teas/[slug]/page.tsx, and listed the teas with generateStaticParams so Next.js could make their pages during the build. Real shops have more teas than they want to build ahead. Here, the shop lists only its popular teas, from its catalog. The catalog's API is a stand-in on the same computer, whose address is in CATALOG_API_URL: it has green, mint and oolong, and says green and mint are popular. The shop is a real Next.js 16.4.3 project, built with next build and run with next start.
app/teas/[slug]/page.tsximport { Suspense } from "react";import { notFound } from "next/navigation";import { getPopularTeas, getTea } from "@/lib/catalog"; export async function generateStaticParams() { const teas = await getPopularTeas(); return teas.map((tea) => ({ slug: tea.slug }));} async function TeaDetails({ params }: Pick<PageProps<"/teas/[slug]">, "params">) { const { slug } = await params; const tea = await getTea(slug); if (!tea) notFound(); return ( <> <h1>{tea.name}</h1> <p>${tea.price} a box</p> </> );} export default function TeaPage({ params }: PageProps<"/teas/[slug]">) { return ( <main> <Suspense fallback={<p>Loading the tea…</p>}> <TeaDetails params={params} /> </Suspense> </main> );}$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (6/6) Finalizing page optimization ... Route (app) Revalidate Expire┌ ○ / 15m 1y├ ○ /_not-found└ /teas/[slug] ├ ◐ /teas/[slug] ├ ○ /teas/green 15m 1y └ ○ /teas/mint 15m 1y ○ (Static) prerendered as static content◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content # The files next build wrote for the prerendered pages, in .next/server/app# (the server's .js code and the .json lists left out), with their sizes.next/server/app/_global-error.html 9.2 kB.next/server/app/_global-error.meta 681 bytes.next/server/app/_global-error.rsc 4.6 kB.next/server/app/_global-error.segments/__PAGE__.segment.rsc 4.3 kB.next/server/app/_global-error.segments/_full.segment.rsc 4.6 kB.next/server/app/_global-error.segments/_tree.segment.rsc 115 bytes.next/server/app/_not-found.html 7.6 kB.next/server/app/_not-found.meta 774 bytes.next/server/app/_not-found.rsc 4.5 kB.next/server/app/_not-found.segments/_full.segment.rsc 4.5 kB.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc 4.0 kB.next/server/app/_not-found.segments/_tree.segment.rsc 172 bytes.next/server/app/index.html 5.6 kB.next/server/app/index.meta 617 bytes.next/server/app/index.rsc 3.4 kB.next/server/app/index.segments/__PAGE__.segment.rsc 3.0 kB.next/server/app/index.segments/_full.segment.rsc 3.3 kB.next/server/app/index.segments/_tree.segment.rsc 115 bytes.next/server/app/teas/[slug].html 1.3 kB.next/server/app/teas/[slug].meta 4.2 kB.next/server/app/teas/[slug].segments/_full.segment.rsc 4.0 kB.next/server/app/teas/[slug].segments/_tree.segment.rsc 245 bytes.next/server/app/teas/[slug].segments/teas/$d$slug/__PAGE__.segment.rsc 3.2 kB.next/server/app/teas/green.html 6.5 kB.next/server/app/teas/green.meta 914 bytes.next/server/app/teas/green.rsc 4.0 kB.next/server/app/teas/green.segments/_full.segment.rsc 4.0 kB.next/server/app/teas/green.segments/_tree.segment.rsc 245 bytes.next/server/app/teas/green.segments/teas/$d$slug/__PAGE__.segment.rsc 3.3 kB.next/server/app/teas/mint.html 6.5 kB.next/server/app/teas/mint.meta 912 bytes.next/server/app/teas/mint.rsc 4.0 kB.next/server/app/teas/mint.segments/_full.segment.rsc 4.0 kB.next/server/app/teas/mint.segments/_tree.segment.rsc 245 bytes.next/server/app/teas/mint.segments/teas/$d$slug/__PAGE__.segment.rsc 3.3 kBLines 5 to 8 are generateStaticParams. Line 6 asks the catalog for the popular teas, with getPopularTeas from lib/catalog.ts. Line 7 turns each tea into an object with a slug: { slug: "green" } and { slug: "mint" }. Lines 10 to 20 are TeaDetails, which waits for params on line 11 and finds the tea on line 12. The page puts it inside a Suspense on lines 25 to 27, with the fallback Loading the tea….
The Terminal tab shows what the build did. In the route table, /teas/[slug] has three lines under it: ○ /teas/green and ○ /teas/mint, one for each listed tea, and ◐ /teas/[slug]. The legend says ○ means (Static) prerendered as static content, and ◐ means (Partial Prerender) prerendered as static HTML with dynamic server-streamed content. Under the output, the list of files the build wrote shows the same thing as files: teas/green.html, teas/mint.html, and teas/[slug].html, the shell for every tea that isn't listed.
generateStaticParams returned two teas, so next build made a page for each: teas/green.html and teas/mint.html. It also made one shell for every slug it didn't list, teas/[slug].html, with Loading the tea… in the tea's place.A bakery bakes its best sellers before it opens, so they're on the shelf when the doors open. It doesn't bake every bread it knows: a rare one is baked when someone first asks for it, and the extra loaves go on the shelf for whoever asks next.
- In real life: The list of breads the baker bakes before opening
- In Next.js: What
generateStaticParamsreturns - In real life: A loaf on the shelf, ready the moment you ask
- In Next.js: A page made during the build, like
teas/green.html - In real life: A rare bread, baked when the first customer orders it
- In Next.js: An unlisted tea, rendered on its first visit
- In real life: Extra loaves of it, put on the shelf for the next customers
- In Next.js: The page Next.js keeps for the next visits
generateStaticParams tells next build which values of a dynamic segment to make pages for. It returns a list of params objects, and the build makes a page for each one. Every other value is rendered on its first visit, from a shell the build also made, and Next.js keeps the result for the next visits.
Listing params at build time
generateStaticParams is a function you export from a page, a layout or a Route Handler in a folder with a dynamic segment. It returns an array. Each object in the array is one page, and each key is a segment's name, the word inside its brackets. Each row is one kind of route, and the shape its list has, from the docs:
| Route | What it returns | One object, for example |
|---|---|---|
app/teas/[slug] | { slug: string }[] | { slug: "green" } |
app/teas/[kind]/[slug] | { kind: string, slug: string }[] | { kind: "green", slug: "sencha" } |
app/guides/[...topic] | { topic: string[] }[] | { topic: ["brewing", "green-tea"] } |
The values are strings, because an address is text; a catch-all segment takes an array of strings, one per segment.
When does it run? The docs give three rules:
- During
next build, before the route's pages are made. The recording kept every request the catalog's stand-in received:/teas/popularcame during the build, once. - In
next dev, when you open the route. - Not again when a page is made again later: its list is fixed by the build.
It can await anything a Server Component can: here, a cached fetch from the catalog. The build then made one page per object. The catalog also got /teas/green and /teas/mint during the build, from getTea on line 12, once for each listed page. Each row is a file the build wrote in .next/server/app:
| File | What it holds | What it was used for |
|---|---|---|
teas/green.html | The whole Green tea page, made during the build. | /teas/green, sent whole: x-nextjs-cache: HIT. |
teas/mint.html | The whole Mint tea page, made during the build. | /teas/mint, sent whole. |
teas/[slug].html | The shell for every other tea: the layout and Loading the tea…. | The first part of the first visit to /teas/oolong. |
teas/[slug].meta | Postponed state, to finish that shell's render once a slug is known. | Used when an unlisted tea is rendered. |
The 15m and 1y next to the listed teas in the route table come from the cache lifetime of the "use cache" functions in lib/catalog.ts. Cache lifetimes and tags teaches those lifetimes. The file names are what this build of Next.js 16.4.3 wrote: read them to see what was made ahead, not as something to build on.
Some now, the rest on demand
Oolong is in the catalog, but not in the list. Go back to the first example, open its Network tab, and pick /teas/oolong, first request. The note above the headers says the response began with .next/server/app/teas/[slug].html, byte for byte: the shell for unlisted teas. Its first part shows All teas and Loading the tea…; further down, a hidden part holds Oolong tea and $5 a box, rendered for this visit. Now pick asked for again: the whole page is in the first part, the headers say x-nextjs-cache: HIT, and the catalog's stand-in got /teas/oolong only once, for the first visit. Next.js kept the page it made, and sent that.
Step through the build and both visits. Before you start, guess: when does the second visitor see the tea?
/teas/oolongafter step 1A blank page
React isn't running yet: clicks do nothing.
The page isn't complete yet.
There's no visitor yet. generateStaticParams asks the catalog for the popular teas, and gets green and mint.
The second visitor sees it at once, at step 8: the page was made on the first visit and kept. The docs call this Incremental Static Regeneration, or ISR: static pages made, or made again, after the build, without a new build. Incremental Static Regeneration covers it, with how a kept page is made again when its data changes.
/teas/oolong starts with the shell for unlisted teas, and the tea streams in. Next.js keeps the finished page, so the second visit gets it whole, and the catalog isn't asked again.Sending the shell for an unlisted param right away is recent. The docs say the shell for unlisted params is served from Next.js 16.3; earlier versions waited for a full render on the server before sending anything. It needs Partial Prefetching on, as in new projects, and params awaited inside Suspense, as on line 11 of the page. Partial Prerendering showed how a shell and its holes are made.
dynamicParams, and teas the shop doesn't have
Without Cache Components, as in projects that don't turn them on, a page can export dynamicParams: true, the default, renders an unlisted param on request; false answers 404 for it. It replaced the fallback option of getStaticPaths in the Pages Router. The docs also say it isn't available with Cache Components. Each button is a version of the tea page, built with real Next.js. Before you click, guess what each does for a tea that isn't listed.
app/teas/[slug]/page.tsxNext.jsapp/teas/[slug]/page.tsximport { notFound } from "next/navigation";import { getPopularTeas, getTea } from "@/lib/catalog"; export const dynamicParams = false; export async function generateStaticParams() { const teas = await getPopularTeas(); return teas.map((tea) => ({ slug: tea.slug }));} export default async function TeaPage({ params }: PageProps<"/teas/[slug]">) { const { slug } = await params; const tea = await getTea(slug); if (!tea) notFound(); return ( <main> <h1>{tea.name}</h1> <p>${tea.price} a box</p> </main> );}GET /teas/oolong404
The whole response is .next/server/app/_not-found.html, a file next build wrote, byte for byte (the Terminal tab lists it).
- Response headers
Varyrsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, Accept-EncodingCache-Controlprivate, no-cache, no-store, max-age=0, must-revalidatex-nextjs-cacheHITx-nextjs-prerender1x-nextjs-stale-time300X-Powered-ByNext.jsContent-Typetext/html; charset=utf-8
The HTML that arrived, one element per line, with the code of its 10 scripts left out
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="preload" as="script" fetchpriority="low" href="/_next/static/chunks/1x6obpmlu1_c2.js">
<script src="/_next/static/chunks/2r4s4h5xul1n-.js" async=""></script>
<script src="/_next/static/chunks/3wbm-994r45kr.js" async=""></script>
<script src="/_next/static/chunks/2e268anpfvdoc.js" async=""></script>
<script src="/_next/static/chunks/turbopack-3-ry682tkyiqk.js" async=""></script>
<script src="/_next/static/chunks/27gjg8e11zukd.js" async=""></script>
<meta name="robots" content="noindex">
<title>Tea shop</title>
<title>404: This page could not be found.</title>
<script src="/_next/static/chunks/0cz1d0mv5g_q7.js" nomodule=""></script>
</head>
<body>
<div hidden="">
<!--$-->
<!--/$-->
</div>
<nav><a href="/">All teas</a></nav>
<div style="font-family:system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center">
<div>
<style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style>
<h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1>
<div style="display:inline-block">
<h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2>
</div>
</div>
</div>
<!--$-->
<!--/$-->
<script id="_R_">…</script>
<script src="/_next/static/chunks/1x6obpmlu1_c2.js" async=""></script>
<script>…</script>
<script>…</script>
</body>
</html>In a project without Cache Components, line 4 turns away every tea that isn't listed. The build lists ● /teas/green and ● /teas/mint, and /teas/oolong answers 404 with the not-found page, though the catalog has oolong.
Without Cache Components, dynamicParams = false on line 4 kept the build to the listed teas: the route table lists ● /teas/green and ● /teas/mint, where ● is (SSG) prerendered as static HTML (uses generateStaticParams), and /teas/oolong answered 404. With Cache Components on, the same line stops the build: Route segment config "dynamicParams" is not compatible with `nextConfig.cacheComponents`. Please remove it. Each row compares the three versions:
| The page | What next build did | What an unlisted tea got |
|---|---|---|
dynamicParams = false, without Cache Components | Builds: ● /teas/green, ● /teas/mint. | 404, the not-found page, even for a tea the catalog has. |
dynamicParams = false, with Cache Components | Stops: the option is not compatible with cacheComponents. | Nothing: there's no build to run. |
notFound() in the page, with Cache Components | Builds: ○ /teas/green, ○ /teas/mint, ◐ /teas/[slug]. | Rendered on request: oolong shows; rooibos 200 with noindex, then 404 when asked again. |
With Cache Components, every param the list leaves out is rendered on request. To turn away a slug that has no tea, do what the migration guide says: call notFound() when the data isn't there, as line 13 does. The first visit to /teas/rooibos answered 200, because the shell had already been sent when notFound() ran, with a noindex tag that tells search engines to skip it; asked for again, it answered 404 from the kept result. Dynamic routes recorded the same, and Not found, forbidden and unauthorized explains the status codes.
Nested dynamic segments
The shop sorts its teas by kind: /teas/green/sencha, /teas/black/assam. That's two dynamic segments, app/teas/[kind]/[slug], and each can list its own params. The kind's layout lists the kinds, and the page lists the popular teas of each kind:
app/teas/[kind]/[slug]/page.tsximport { Suspense } from "react";import { notFound } from "next/navigation";import { getPopularTeas, getTea } from "@/lib/catalog"; export async function generateStaticParams({ params }: { params: { kind: string } }) { const teas = await getPopularTeas(params.kind); return teas.map((tea) => ({ slug: tea.slug }));} async function TeaName({ params }: Pick<PageProps<"/teas/[kind]/[slug]">, "params">) { const { kind, slug } = await params; const tea = await getTea(kind, slug); if (!tea) notFound(); return <p>{tea.name}</p>;} export default function TeaPage({ params }: PageProps<"/teas/[kind]/[slug]">) { return ( <Suspense fallback={<p>Loading the tea…</p>}> <TeaName params={params} /> </Suspense> );}$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (9/9) Finalizing page optimization ... Route (app)┌ ○ /├ ○ /_not-found└ /teas/[kind]/[slug] ├ ◐ /teas/[kind]/[slug] ├ ◐ /teas/green/[slug] ├ ○ /teas/green/sencha └ ◐ [+3 more paths] ○ (Static) prerendered as static content◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content # The files next build wrote for the prerendered pages, in .next/server/app# (the server's .js code and the .json lists left out), with their sizes.next/server/app/_global-error.html 9.2 kB.next/server/app/_global-error.meta 681 bytes.next/server/app/_global-error.rsc 4.6 kB.next/server/app/_global-error.segments/__PAGE__.segment.rsc 4.3 kB.next/server/app/_global-error.segments/_full.segment.rsc 4.6 kB.next/server/app/_global-error.segments/_tree.segment.rsc 115 bytes.next/server/app/_not-found.html 7.6 kB.next/server/app/_not-found.meta 774 bytes.next/server/app/_not-found.rsc 4.5 kB.next/server/app/_not-found.segments/_full.segment.rsc 4.5 kB.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc 4.0 kB.next/server/app/_not-found.segments/_tree.segment.rsc 172 bytes.next/server/app/index.html 5.0 kB.next/server/app/index.meta 617 bytes.next/server/app/index.rsc 3.0 kB.next/server/app/index.segments/__PAGE__.segment.rsc 2.7 kB.next/server/app/index.segments/_full.segment.rsc 3.0 kB.next/server/app/index.segments/_tree.segment.rsc 115 bytes.next/server/app/teas/[kind]/[slug].html 1.4 kB.next/server/app/teas/[kind]/[slug].meta 4.8 kB.next/server/app/teas/[kind]/[slug].segments/_full.segment.rsc 4.5 kB.next/server/app/teas/[kind]/[slug].segments/_tree.segment.rsc 325 bytes.next/server/app/teas/[kind]/[slug].segments/teas/$d$kind/$d$slug/__PAGE__.segment.rsc 3.6 kB.next/server/app/teas/black/[slug].html 1.4 kB.next/server/app/teas/black/[slug].meta 4.7 kB.next/server/app/teas/black/[slug].segments/_full.segment.rsc 4.6 kB.next/server/app/teas/black/[slug].segments/_tree.segment.rsc 325 bytes.next/server/app/teas/black/[slug].segments/teas/$d$kind/$d$slug/__PAGE__.segment.rsc 3.7 kB.next/server/app/teas/black/assam.html 7.2 kB.next/server/app/teas/black/assam.meta 1.1 kB.next/server/app/teas/black/assam.rsc 4.6 kB.next/server/app/teas/black/assam.segments/_full.segment.rsc 4.6 kB.next/server/app/teas/black/assam.segments/_tree.segment.rsc 325 bytes.next/server/app/teas/black/assam.segments/teas/$d$kind/$d$slug/__PAGE__.segment.rsc 3.7 kB.next/server/app/teas/green/[slug].html 1.4 kB.next/server/app/teas/green/[slug].meta 4.8 kB.next/server/app/teas/green/[slug].segments/_full.segment.rsc 4.6 kB.next/server/app/teas/green/[slug].segments/_tree.segment.rsc 325 bytes.next/server/app/teas/green/[slug].segments/teas/$d$kind/$d$slug/__PAGE__.segment.rsc 3.7 kB.next/server/app/teas/green/matcha.html 7.2 kB.next/server/app/teas/green/matcha.meta 1.1 kB.next/server/app/teas/green/matcha.rsc 4.6 kB.next/server/app/teas/green/matcha.segments/_full.segment.rsc 4.6 kB.next/server/app/teas/green/matcha.segments/_tree.segment.rsc 325 bytes.next/server/app/teas/green/matcha.segments/teas/$d$kind/$d$slug/__PAGE__.segment.rsc 3.7 kB.next/server/app/teas/green/sencha.html 7.2 kB.next/server/app/teas/green/sencha.meta 1.1 kB.next/server/app/teas/green/sencha.rsc 4.6 kB.next/server/app/teas/green/sencha.segments/_full.segment.rsc 4.6 kB.next/server/app/teas/green/sencha.segments/_tree.segment.rsc 325 bytes.next/server/app/teas/green/sencha.segments/teas/$d$kind/$d$slug/__PAGE__.segment.rsc 3.7 kBIn app/teas/[kind]/layout.tsx, lines 4 to 7 list the kinds from the catalog: green and black. In app/teas/[kind]/[slug]/page.tsx, the page's generateStaticParams on line 5 takes an argument: params, with one kind the layout listed. It's a plain object, not a promise, and it holds only the parent's params. Line 6 asks the catalog for that kind's popular teas, and line 7 returns their slugs.
The docs say a child's generateStaticParams runs once for each set of params the parent's returns. The stand-in's requests show it: during the build, /kinds once, then /kinds/green/popular once and /kinds/black/popular once. The route table shows only a few of the pages, then [+3 more paths]; the list of files has all of them: teas/green/sencha.html, teas/green/matcha.html, teas/black/assam.html, a shell for each kind's unlisted teas, like teas/green/[slug].html, and teas/[kind]/[slug].html for kinds that aren't listed.
generateStaticParams lists the kinds. The page's runs once for each, with that kind in params, and lists its teas: three pages, teas/green/sencha, teas/green/matcha and teas/black/assam.Pick the requests in the Network tab. /teas/green/gyokuro, a green tea that isn't listed, began with teas/green/[slug].html: the heading green teas was already in it, with only the tea left to stream. /teas/herbal/mint, a kind that isn't listed, began with teas/[kind]/[slug].html, with both fallbacks. There are two ways to list nested params, and the docs show both:
- From the top down, as here: the parent lists its segment, and the child lists its own for each parent value.
- From the bottom up: the page alone returns every combination, like
{ kind: "green", slug: "sencha" }. A layout can list params for its own segment and the ones above it, never for the segments below it.
What the build needs with Cache Components
Dynamic routes showed that with Cache Components on, a page that awaits params needs either the list or a Suspense. With a list, there's more to get right. Each button is one version of the tea page, built with real Next.js. Before you click, guess which version gives an unlisted tea its fallback at once.
app/teas/[slug]/page.tsxNext.jsapp/teas/[slug]/page.tsximport { Suspense } from "react";import { notFound } from "next/navigation";import { getPopularTeas, getTea } from "@/lib/catalog"; export async function generateStaticParams() { const teas = await getPopularTeas(); return teas.map((tea) => ({ slug: tea.slug }));} async function TeaDetails({ params }: Pick<PageProps<"/teas/[slug]">, "params">) { const { slug } = await params; const tea = await getTea(slug); if (!tea) notFound(); return ( <> <h1>{tea.name}</h1> <p>${tea.price} a box</p> </> );} export default function TeaPage({ params }: PageProps<"/teas/[slug]">) { return ( <main> <Suspense fallback={<p>Loading the tea…</p>}> <TeaDetails params={params} /> </Suspense> </main> );}$ next build▲ Next.js 16.4.3 (Turbopack)- Cache Components enabled- Partial Prefetching enabled Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (6/6) Finalizing page optimization ... Route (app) Revalidate Expire┌ ○ / 15m 1y├ ○ /_not-found└ /teas/[slug] ├ ◐ /teas/[slug] ├ ○ /teas/green 15m 1y └ ○ /teas/mint 15m 1y ○ (Static) prerendered as static content◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed content # The files next build wrote for the prerendered pages, in .next/server/app# (the server's .js code and the .json lists left out), with their sizes.next/server/app/_global-error.html 9.2 kB.next/server/app/_global-error.meta 681 bytes.next/server/app/_global-error.rsc 4.6 kB.next/server/app/_global-error.segments/__PAGE__.segment.rsc 4.3 kB.next/server/app/_global-error.segments/_full.segment.rsc 4.6 kB.next/server/app/_global-error.segments/_tree.segment.rsc 115 bytes.next/server/app/_not-found.html 7.6 kB.next/server/app/_not-found.meta 774 bytes.next/server/app/_not-found.rsc 4.5 kB.next/server/app/_not-found.segments/_full.segment.rsc 4.5 kB.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc 4.0 kB.next/server/app/_not-found.segments/_tree.segment.rsc 172 bytes.next/server/app/index.html 5.6 kB.next/server/app/index.meta 617 bytes.next/server/app/index.rsc 3.4 kB.next/server/app/index.segments/__PAGE__.segment.rsc 3.0 kB.next/server/app/index.segments/_full.segment.rsc 3.3 kB.next/server/app/index.segments/_tree.segment.rsc 115 bytes.next/server/app/teas/[slug].html 1.3 kB.next/server/app/teas/[slug].meta 4.2 kB.next/server/app/teas/[slug].segments/_full.segment.rsc 4.0 kB.next/server/app/teas/[slug].segments/_tree.segment.rsc 245 bytes.next/server/app/teas/[slug].segments/teas/$d$slug/__PAGE__.segment.rsc 3.2 kB.next/server/app/teas/green.html 6.5 kB.next/server/app/teas/green.meta 914 bytes.next/server/app/teas/green.rsc 4.0 kB.next/server/app/teas/green.segments/_full.segment.rsc 4.0 kB.next/server/app/teas/green.segments/_tree.segment.rsc 245 bytes.next/server/app/teas/green.segments/teas/$d$slug/__PAGE__.segment.rsc 3.3 kB.next/server/app/teas/mint.html 6.5 kB.next/server/app/teas/mint.meta 912 bytes.next/server/app/teas/mint.rsc 4.0 kB.next/server/app/teas/mint.segments/_full.segment.rsc 4.0 kB.next/server/app/teas/mint.segments/_tree.segment.rsc 245 bytes.next/server/app/teas/mint.segments/teas/$d$slug/__PAGE__.segment.rsc 3.3 kBThe popular teas are listed on lines 5 to 8, and TeaDetails waits for params inside the Suspense on lines 25 to 27. The build makes ○ /teas/green and ○ /teas/mint, and a shell for every other tea, teas/[slug].html, with Loading the tea….
Each row is one version: what the build did, and what the first visit to /teas/oolong got.
| The page | What next build did | The first visit to an unlisted tea |
|---|---|---|
params inside Suspense, popular teas listed | Listed teas ○, a shell for the rest | The shell with Loading the tea… at once, then the tea; the second visit got the kept page. |
params at the top, popular teas listed | Listed teas ○, an empty shell for the rest | Nothing until the whole page was rendered. |
No generateStaticParams | Only ◐ /teas/[slug] | The shell, then the tea, on every visit: never a kept page. |
generateStaticParams returns [] | Stops | Nothing: there's no build to run. |
So the build needs three things from a route with a dynamic segment:
- At least one param. An empty list stops the build: When using Cache Components, all `generateStaticParams` functions must return at least one result. The build renders the route with a real param to check it, for example that it doesn't read
cookies()outsideSuspense. paramsawaited insideSuspense. Then the shell for unlisted params holds the layout and the fallback. Awaited at the top, the shell was empty, and the first visitor waited for the whole page.- The list itself, to keep pages. Without
generateStaticParams, every visit to/teas/greenwas rendered again, from the shell: the migration guide says removing the export opts the route out of ISR, even when its data is cached.
Choosing which params to list
Every page you list makes the build longer and has to be stored and deployed. The docs suggest listing the pages that gain most from being ready ahead, like the most visited ones, and letting the rest be made on their first visit. For the shop:
- List the popular teas, so most visitors get a page made during the build.
- Leave the rest on demand. The first visitor gets the shell at once, and everyone after gets the kept page.
- Call
notFound()for slugs with no data, since Cache Components renders every unlisted slug.
Sort these requests by what their response starts with, as the recordings showed:
/teas/green/teas/mint/teas/oolong, its first visit/teas/oolong, asked for again/teas/green/sencha, in the shop with kinds/teas/green/gyokuro, its first visit
Sort each request: does its response start with a page the build made, with a shell for params the build didn't list, or with a page kept from an earlier visit?
Common mix-ups
- "Only the listed params work." No. Every other value is rendered on its first visit: oolong showed, though it wasn't listed.
- "An unlisted page is rendered for every visit." No. Next.js kept it: the second visit was a
HIT, and the catalog was asked once. - "
dynamicParams = falseblocks unlisted params." Only without Cache Components. With them, the build stops; callnotFound()instead. - "Return
[]to make every page on demand." Not with Cache Components: the build stops. Return at least one real param. - "A child's
generateStaticParamsruns once." No. It runs once for each set of params its parent returns: twice for two kinds. - "The child gets
paramsas a promise." No. IngenerateStaticParams, the parent's params are a plain object. Pages and layouts get a promise.
Practice exercises
These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints. They use the same stand-in catalog: green, mint and oolong, with green and mint popular.
Which tea pages does next build make ahead for this page?
import { Suspense } from "react";
import { notFound } from "next/navigation";
import { getPopularTeas, getTea } from "@/lib/catalog";
export async function generateStaticParams() {
const teas = await getPopularTeas();
return teas.map((tea) => ({ slug: tea.slug }));
}
async function TeaDetails({ params }: Pick<PageProps<"/teas/[slug]">, "params">) {
const { slug } = await params;
const tea = await getTea(slug);
if (!tea) notFound();
return (
<>
<h1>{tea.name}</h1>
<p>${tea.price} a box</p>
</>
);
}
export default function TeaPage({ params }: PageProps<"/teas/[slug]">) {
return (
<main>
<Suspense fallback={<p>Loading the tea…</p>}>
<TeaDetails params={params} />
</Suspense>
</main>
);
}
/teas/green and /teas/mint. The recorded build lists ○ /teas/green and ○ /teas/mint, and wrote teas/green.html and teas/mint.html.
The first visitor opens /teas/oolong. Besides the nav, what does the first part of the HTML show?
import { Suspense } from "react";
import { notFound } from "next/navigation";
import { getPopularTeas, getTea } from "@/lib/catalog";
export async function generateStaticParams() {
const teas = await getPopularTeas();
return teas.map((tea) => ({ slug: tea.slug }));
}
async function TeaDetails({ params }: Pick<PageProps<"/teas/[slug]">, "params">) {
const { slug } = await params;
const tea = await getTea(slug);
if (!tea) notFound();
return (
<>
<h1>{tea.name}</h1>
<p>${tea.price} a box</p>
</>
);
}
export default function TeaPage({ params }: PageProps<"/teas/[slug]">) {
return (
<main>
<Suspense fallback={<p>Loading the tea…</p>}>
<TeaDetails params={params} />
</Suspense>
</main>
);
}
Loading the tea…, the fallback on line 25. The response began with teas/[slug].html, and Oolong tea streamed in after it.
Two visitors open /teas/oolong, one after the other. How many times does the catalog get a request for /teas/oolong?
Once. The first visit rendered the page and asked the catalog for /teas/oolong; the second got the kept page, with x-nextjs-cache: HIT. The stand-in's recording lists /teas/oolong once.
With Cache Components on, this page stops the build. Which line do you remove?
import { Suspense } from "react";
import { notFound } from "next/navigation";
import { getPopularTeas, getTea } from "@/lib/catalog";
export const dynamicParams = false;
export async function generateStaticParams() {
const teas = await getPopularTeas();
return teas.map((tea) => ({ slug: tea.slug }));
}
async function TeaDetails({ params }: Pick<PageProps<"/teas/[slug]">, "params">) {
const { slug } = await params;
const tea = await getTea(slug);
if (!tea) notFound();
return (
<>
<h1>{tea.name}</h1>
<p>${tea.price} a box</p>
</>
);
}
export default function TeaPage({ params }: PageProps<"/teas/[slug]">) {
return (
<main>
<Suspense fallback={<p>Loading the tea…</p>}>
<TeaDetails params={params} />
</Suspense>
</main>
);
}
import { Suspense } from "react";
import { notFound } from "next/navigation";
import { getPopularTeas, getTea } from "@/lib/catalog";
export async function generateStaticParams() {
const teas = await getPopularTeas();
return teas.map((tea) => ({ slug: tea.slug }));
}
async function TeaDetails({ params }: Pick<PageProps<"/teas/[slug]">, "params">) {
const { slug } = await params;
const tea = await getTea(slug);
if (!tea) notFound();
return (
<>
<h1>{tea.name}</h1>
<p>${tea.price} a box</p>
</>
);
}
export default function TeaPage({ params }: PageProps<"/teas/[slug]">) {
return (
<main>
<Suspense fallback={<p>Loading the tea…</p>}>
<TeaDetails params={params} />
</Suspense>
</main>
);
}
Remove line 5, export const dynamicParams = false;. The recorded build stopped there: it is not compatible with nextConfig.cacheComponents. Without it, the page builds, and notFound() on line 13 handles slugs with no tea.
This is app/teas/[kind]/layout.tsx, and the page below it has its own generateStaticParams. How many times does the page's run during the build?
import { Suspense } from "react";
import { getKinds } from "@/lib/catalog";
export async function generateStaticParams() {
const kinds = await getKinds();
return kinds.map((kind) => ({ kind }));
}
async function KindHeading({ params }: Pick<LayoutProps<"/teas/[kind]">, "params">) {
const { kind } = await params;
return <h1>{kind} teas</h1>;
}
export default function KindLayout({ children, params }: LayoutProps<"/teas/[kind]">) {
return (
<section>
<Suspense fallback={<h1>Loading the kind…</h1>}>
<KindHeading params={params} />
</Suspense>
{children}
</section>
);
}
Twice: once with { kind: "green" } and once with { kind: "black" }. The stand-in got /kinds/green/popular and /kinds/black/popular, once each, during the build.
This page wants every tea made on demand, so it returns []. With Cache Components on, the build stops. How many params must the list hold, at least?
import { Suspense } from "react";
import { notFound } from "next/navigation";
import { getTea } from "@/lib/catalog";
export async function generateStaticParams() {
return [];
}
async function TeaDetails({ params }: Pick<PageProps<"/teas/[slug]">, "params">) {
const { slug } = await params;
const tea = await getTea(slug);
if (!tea) notFound();
return (
<>
<h1>{tea.name}</h1>
<p>${tea.price} a box</p>
</>
);
}
export default function TeaPage({ params }: PageProps<"/teas/[slug]">) {
return (
<main>
<Suspense fallback={<p>Loading the tea…</p>}>
<TeaDetails params={params} />
</Suspense>
</main>
);
}
import { Suspense } from "react";
import { notFound } from "next/navigation";
import { getPopularTeas, getTea } from "@/lib/catalog";
export async function generateStaticParams() {
const teas = await getPopularTeas();
return teas.map((tea) => ({ slug: tea.slug }));
}
async function TeaDetails({ params }: Pick<PageProps<"/teas/[slug]">, "params">) {
const { slug } = await params;
const tea = await getTea(slug);
if (!tea) notFound();
return (
<>
<h1>{tea.name}</h1>
<p>${tea.price} a box</p>
</>
);
}
export default function TeaPage({ params }: PageProps<"/teas/[slug]">) {
return (
<main>
<Suspense fallback={<p>Loading the tea…</p>}>
<TeaDetails params={params} />
</Suspense>
</main>
);
}
1. With Cache Components, every generateStaticParams must return at least one result. Return the popular teas, as the shop does, and the build makes their pages and a shell for the rest.
Check your understanding
For each question, ask which values the build knew, and what the build made for the rest.
Question 1 of 7What does
generateStaticParamsinapp/teas/[slug]/page.tsxreturn?Choose an answer to see the explanation.
Question 2 of 7The build prints
/teas/[slug]with◐ /teas/[slug],○ /teas/greenand○ /teas/mintunder it. What is◐ /teas/[slug]for?Choose an answer to see the explanation.
Question 3 of 7A second visitor asks for
/teas/oolong, which isn't listed. What do they get?Choose an answer to see the explanation.
Question 4 of 7This page runs in a project with Cache Components on. What does
next builddo?Read the code, then predictTSXimport { Suspense } from "react"; import { notFound } from "next/navigation"; import { getPopularTeas, getTea } from "@/lib/catalog"; export const dynamicParams = false; export async function generateStaticParams() { const teas = await getPopularTeas(); return teas.map((tea) => ({ slug: tea.slug })); } async function TeaDetails({ params }: Pick<PageProps<"/teas/[slug]">, "params">) { const { slug } = await params; const tea = await getTea(slug); if (!tea) notFound(); return ( <> <h1>{tea.name}</h1> <p>${tea.price} a box</p> </> ); } export default function TeaPage({ params }: PageProps<"/teas/[slug]">) { return ( <main> <Suspense fallback={<p>Loading the tea…</p>}> <TeaDetails params={params} /> </Suspense> </main> ); }Choose an answer to see the explanation.
Question 5 of 7In the shop with kinds, the layout's
generateStaticParamsreturnsgreenandblack. How many times does the page's owngenerateStaticParamsrun?Read the code, then predictTSXimport { Suspense } from "react"; import { notFound } from "next/navigation"; import { getPopularTeas, getTea } from "@/lib/catalog"; export async function generateStaticParams({ params }: { params: { kind: string } }) { const teas = await getPopularTeas(params.kind); return teas.map((tea) => ({ slug: tea.slug })); } async function TeaName({ params }: Pick<PageProps<"/teas/[kind]/[slug]">, "params">) { const { kind, slug } = await params; const tea = await getTea(kind, slug); if (!tea) notFound(); return <p>{tea.name}</p>; } export default function TeaPage({ params }: PageProps<"/teas/[kind]/[slug]">) { return ( <Suspense fallback={<p>Loading the tea…</p>}> <TeaName params={params} /> </Suspense> ); }Choose an answer to see the explanation.
Question 6 of 7With Cache Components on, this
generateStaticParamsreturns[]. What happens?Read the code, then predictTSXimport { Suspense } from "react"; import { notFound } from "next/navigation"; import { getTea } from "@/lib/catalog"; export async function generateStaticParams() { return []; } async function TeaDetails({ params }: Pick<PageProps<"/teas/[slug]">, "params">) { const { slug } = await params; const tea = await getTea(slug); if (!tea) notFound(); return ( <> <h1>{tea.name}</h1> <p>${tea.price} a box</p> </> ); } export default function TeaPage({ params }: PageProps<"/teas/[slug]">) { return ( <main> <Suspense fallback={<p>Loading the tea…</p>}> <TeaDetails params={params} /> </Suspense> </main> ); }Choose an answer to see the explanation.
Question 7 of 7This page waits for
paramson line 10, outside anySuspense. What does the first visitor to/teas/oolongget first?Read the code, then predictTSXimport { notFound } from "next/navigation"; import { getPopularTeas, getTea } from "@/lib/catalog"; export async function generateStaticParams() { const teas = await getPopularTeas(); return teas.map((tea) => ({ slug: tea.slug })); } export default async function TeaPage({ params }: PageProps<"/teas/[slug]">) { const { slug } = await params; const tea = await getTea(slug); if (!tea) notFound(); return ( <main> <h1>{tea.name}</h1> <p>${tea.price} a box</p> </main> ); }Choose an answer to see the explanation.
Key takeaways
generateStaticParamsreturns a list of params objects;next buildmakes a page for each, and a shell for every other value.- An unlisted value is rendered on its first visit, from that shell, and the page is kept: the second visit got it whole.
dynamicParamsdoesn't work with Cache Components: the build stops. CallnotFound()for values with no data.- A child's
generateStaticParamsruns once for each set of its parent's params, which it gets as a plain object. - With Cache Components: return at least one param, await
paramsinsideSuspense, and keep the list, or no page is kept.
Remember the one-liner.
List the pages worth making ahead; the rest are made on their first visit and kept.
Coming next: Cache Components. It explains the model behind "use cache", where everything is fresh unless you cache it, and what changed from older versions.