Cache Components
Turn on Cache Components and learn its model: work is fresh for each request unless you cache it, and you cache, stream, or both, with the build checking.
- 01Explain dynamic by defaultSay which work Next.js finishes during the build, and which it leaves fresh for each request unless you cache it.
- 02Cache, stream, or bothChoose "use cache", Suspense, or both for each part of a page, and predict how often the data source is asked.
- 03Read the build and move old codeRead what next build prints with Cache Components on, and replace older flags and route segment config with the new model.
Fresh unless you cache it
A cache is a stored copy of a result, used again instead of doing the work again. Cache Components is the Next.js feature that decides how caching works in an app, and every new project has it on. Its rule is short: work that waits for data, or for the request, is done fresh for each request, unless you mark it to be cached. Here is the tea shop's Teas page, a real Next.js 16.4.3 project, built with next build and run with next start. The result under the files is a recording of what happened.
app/teas/page.tsximport { Suspense } from "react";import { getMenu, getStock } from "@/lib/supplier"; async function Menu() { const teas = await getMenu(); return ( <ul> {teas.map((tea) => ( <li key={tea.id}> {tea.name}: ${tea.price} </li> ))} </ul> );} async function Stock() { const { boxes } = await getStock(); return <p>In stock today: {boxes} boxes</p>;} export default function TeasPage() { return ( <main> <h1>Our teas</h1> <Menu /> <Suspense fallback={<p>Checking stock…</p>}> <Stock /> </Suspense> </main> );}- Green tea: $4
- Mint tea: $3
- Lemon tea: $3
In stock today: 12 boxes
The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
In app/teas/page.tsx, the file shown first, Menu on lines 4 to 15 waits for getMenu() and lists the teas. Stock, on lines 17 to 20, waits for getStock(). The page itself shows a heading on line 25, the menu on line 26, and the stock inside Suspense on lines 27 to 29.
Now open lib/supplier.ts. Both functions ask the tea supplier's API, whose address is in the environment variable SUPPLIER_API_URL. getMenu, on lines 7 to 12, starts with "use cache" on line 8. That line is a directive: a string at the top of a function that tells Next.js how to treat it. This one says: keep what this function returns, and use that copy again. Line 9, cacheLife("hours"), says how long the copy stays fresh. getStock, on lines 14 to 17, has neither line, so nothing keeps its answer.
The supplier in this recording is a stand-in that ran on the same computer, and it kept a list of every request it got. The app was built once and visited twice. Each row is one kind of request the supplier received:
| Request to the supplier | During next build | During the two visits |
|---|---|---|
GET /menu | Once. getMenu is cached, so the build ran it and kept the list. | Never. Both visits got the list the build kept. |
GET /stock | Never. getStock isn't cached, so the build left it for request time. | Twice: once for each visit. |
Open the Network tab. Both visits got the same HTML first: the heading, the three teas, and Checking stock…. The stock arrived later in the same response and took the fallback's place. Then open the Terminal tab: ◐ /teas says the page was prerendered as static HTML with dynamic server-streamed content. The 1h and 1d after it come from the menu's cacheLife("hours"); How Next.js renders pages reads those two columns.
getMenu(), which is cached, so the build made it too. The stock waits for the uncached getStock(), so it is made for each visit, inside Suspense.The sign over the door was painted before the shop opened. The price board is chalked in the morning. Once it's a few hours old, the next customer still reads the old board while a clerk chalks a new one. But when a customer asks how many boxes of mint tea are left, the clerk phones the warehouse every time, because that number changes all day. When a regular asks for their usual tea, the clerk reads their name card, then looks the tea up in a shared binder: if another clerk already wrote that tea's details there, nobody phones the supplier again.
- In real life: The shop's name painted over the door
- In Next.js: The heading: prerendered during the build
- In real life: The price board, chalked in the morning and redone once it's a few hours old
- In Next.js:
getMenuwith"use cache"andcacheLife("hours") - In real life: Phoning the warehouse each time a customer asks about stock
- In Next.js:
Stock, insideSuspense, run for each request - In real life: Looking up a regular's usual tea in the shared tea binder
- In Next.js: A cookie read for each request, and a cached
getTea(id)
With Cache Components on, every piece of work that waits for data or for the request is fresh for each request, unless you cache it. Next.js prerenders everything that waits for nothing. For each piece that waits, you choose: cache it with "use cache", stream it with Suspense, or both. The build checks that every piece has an answer.
Turning Cache Components on
Cache Components is one option in next.config.ts, the file where a project sets Next.js's options. This is the file that create-next-app 16.4.3 writes with its recommended answers. The tea shop's examples use it unless they show their own:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
cacheComponents: true,
partialPrefetching: true,
};
export default nextConfig;
Line 1 imports the type that describes the options. Line 5, cacheComponents: true, turns on Cache Components. Line 6, partialPrefetching: true, turns on Partial Prefetching: when a link shows on the screen, the router prefetches only the linked route's App Shell, the part that doesn't depend on the link's address, shared by every link to that route (Linking and navigating showed it). For a route that reads nothing about the visitor, that's the static shell the build made; for one that reads cookies, it's rendered for each visitor's session (Instant navigations and partial prefetching records both). Partial Prefetching needs Cache Components: with line 6 and without line 5, next build stops at once with partialPrefetching requires cacheComponents to be enabled.
Line 5 turns on several things at once:
- The directive and its helpers.
"use cache", and the functions that go with it:cacheLifefor how long a copy stays fresh, andcacheTagfor a name you can expire later. - Partial Prerendering for every route. The build makes a static shell for each page, and leaves holes where parts stream in for each request (Partial Prerendering).
- The build's check. Work that waits for data or for the request, outside
Suspenseand outside a cache, stops the build with an error that lists the fixes. - Hidden pages after a navigation. The router keeps up to two pages you left, hidden with React's Activity and with their state, so Back shows them as they were (Layouts and pages).
The build says when the options are on: with this file, its first lines are - Cache Components enabled and - Partial Prefetching enabled, as every recorded Terminal tab above shows. Here is the same shop with a next.config.ts that sets no options:
next.config.tsimport type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */}; export default nextConfig;$ next build▲ Next.js 16.4.3 (Turbopack) Creating an optimized production build ... > Build error occurredError: Turbopack build failed with 1 error:./lib/supplier.ts:8:3Error: To use "use cache", please enable the feature flag `cacheComponents` in your Next.js config. Read more: https://nextjs.org/docs/app/api-reference/directives/use-cache#usage 6 | 7 | export async function getMenu(): Promise<Tea[]> {> 8 | "use cache"; | ^^^^^^^^^^^ 9 | cacheLife("hours"); 10 | const response = await fetch(`${api}/menu`); 11 | return response.json(); Ecmascript file had an error Import trace: Server Component: ./lib/supplier.ts ./app/teas/page.tsx at ignore-listed framesThe build stopped at line 8 of lib/supplier.ts: To use "use cache", please enable the feature flag cacheComponents in your Next.js config. In plain words, "use cache" only exists when Cache Components is on. Without it, Next.js uses its previous caching model, which Caching before Cache Components teaches.
New projects get both options since Next.js 16.4, released on October 6, 2026. Its release post recommends Cache Components for every Next.js app, and says it becomes the default in Next.js 17. The docs add that both options will then be removed, because both features will always be on.
Dynamic by default
Dynamic work is work Next.js does when a request comes in, instead of during the build. With Cache Components, every piece of work that waits for data is dynamic unless you cache it. That's what "dynamic by default" means. The clearest way to see it is to turn the option off. This is the same shop with the default next.config.ts again, but with "use cache" and cacheLife removed, so it builds:
next.config.tsimport type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */}; export default nextConfig;$ next build▲ Next.js 16.4.3 (Turbopack) Creating an optimized production build ...✓ Compiled successfully Running TypeScript ... Finished TypeScript ... Collecting page data ...✓ Generating static pages (4/4) Finalizing page optimization ... Route (app)┌ ○ /├ ○ /_not-found└ ○ /teas ○ (Static) prerendered as static contentThe build printed ○ /teas: prerendered as static content. Nothing told Next.js to keep the stock, and yet it ran getStock() during the build and wrote the answer into the page. The stand-in supplier was asked for /menu and /stock once each, during the build, and never during the two visits. Open the Network tab: both visits got In stock today: 12 boxes from the build, with Cache-Control: s-maxage=31536000, which lets shared caches keep the page for a year. (Partial Prerendering found the same with the Mint tea page.) Each row compares the two projects:
| Question | With Cache Components | Without Cache Components |
|---|---|---|
What the build prints for /teas | ◐ /teas: a static shell, with the stock streamed in. | ○ /teas, prerendered as static content: the whole page, stock included, from the build. |
| When the supplier is asked for the stock | For each visit: twice for two visits, never during the build. | Once, during the build. Neither visit asked. |
| What a visitor sees for the stock | Today's number, fetched for this visit. | The number from the day of the build, until the page is prerendered again. |
| How you get the other behavior | Cache getStock with "use cache", if a slightly old number is fine. | Opt out with route settings, like export const dynamic = "force-dynamic" on the page. |
Without Cache Components, data you fetched during the build quietly became part of the page, and you added settings to opt out. With it, nothing that waits is kept unless you say so. Next.js's release post for version 16 puts it this way: caching with Cache Components is entirely opt-in, and dynamic code runs at request time by default.
What counts as work that waits
Not everything is dynamic. Next.js runs every page during the build, and finishes all the work it can. It only stops at work whose answer could differ for each request. Each version below is the shop's home page, with one line of work in a Banner. Before you click, guess which versions build.
app/page.tsxNext.jsapp/page.tsxconst teas = ["Green tea", "Mint tea", "Lemon tea"]; function Banner() { const count = teas.length; return <p>{count} teas on the menu.</p>;} export default function Home() { return ( <main> <h1>Tea shop</h1> <Banner /> </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 (4/4) Finalizing page optimization ... Route (app) Revalidate Expire┌ ○ /├ ○ /_not-found└ ◐ /teas 1h 1d ○ (Static) prerendered as static content◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed contentLine 4 counts a list written in the file. That waits for nothing, so the build finishes it and prints ○ /, with 3 teas on the menu. in the HTML.
The first two build into ○ /: counting a list in memory and reading a file with readFileSync finish at once, the same way every time, so their answers are in the HTML. The other three stop the build:
- Data from elsewhere: an uncached
fetch, or a database query. The answer can change between requests. - The request itself:
cookies(),headers(),searchParams,paramsthe build doesn't know, andconnection(). They don't exist until a request comes in (Request-time APIs). - Values that change on every run:
new Date(),Math.random()andcrypto.randomUUID(). Next.js calls them unstable values, and stops with an error of their own.
Speed has nothing to do with it. A slow function that waits for nothing still builds, and a fast uncached fetch still needs an answer from you. The build reacts to work that has to wait, outside Suspense and outside a cache.
Cache, stream, or both
When the build stops, its error lists the fixes. The two you'll use most are [stream], put the part in Suspense, and [cache], cache the access with "use cache". (The third, [block], allows a route to make visitors wait for the whole page; How Next.js renders pages shows it.) Each answer fits a different kind of data:
- Cache it when every visitor can share the same answer for a while, like the menu. One copy, made during the build, goes into the static shell.
- Stream it when it must be fresh, or belongs to one visitor, like today's stock or a cart. It runs for each request, and the fallback holds its place in the shell.
- Both when the part depends on the request, but the work behind it can be shared by everyone who sends the same value. Read the request inside
Suspense, then pass the value to a cached function.
"Both" is the one people miss. The shop now shows each visitor their favorite tea, from a cookie named favorite, with the tea's details from the supplier:
app/teas/page.tsximport { Suspense } from "react";import { cookies } from "next/headers";import { getMenu, getStock, getTea } from "@/lib/supplier"; async function Menu() { const teas = await getMenu(); return ( <ul> {teas.map((tea) => ( <li key={tea.id}> {tea.name}: ${tea.price} </li> ))} </ul> );} async function Stock() { const { boxes } = await getStock(); return <p>In stock today: {boxes} boxes</p>;} async function Favorite() { const id = (await cookies()).get("favorite")?.value; if (!id) return <p>Pick a favorite tea.</p>; const tea = await getTea(id); return <p>Your favorite: {tea.name}, ${tea.price}</p>;} export default function TeasPage() { return ( <main> <h1>Our teas</h1> <Menu /> <Suspense fallback={<p>Checking stock…</p>}> <Stock /> </Suspense> <Suspense fallback={<p>Finding your favorite…</p>}> <Favorite /> </Suspense> </main> );}- Green tea: $4
- Mint tea: $3
- Lemon tea: $3
In stock today: 12 boxes
Pick a favorite tea.
The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
In app/teas/page.tsx, Favorite on lines 23 to 28 reads the cookie on line 24. Without one, line 25 asks the visitor to pick a tea. Otherwise line 26 calls getTea(id). Lines 38 to 40 put Favorite in its own Suspense, because a cookie only exists at request time. In lib/supplier.ts, getTea on lines 19 to 24 is cached: "use cache" on line 20, cacheLife("hours") on line 21.
Four visitors opened the page: one with no cookie, then Asha and Ben, whose favorite is mint, then Chen, whose favorite is green. Pick each of them above the page in the Browser tab. The supplier was asked for /teas/mint once, on Asha's visit, and for /teas/green once, on Chen's. Ben got Asha's copy. The cookie was read for every visit, but the work behind it was shared. That's because the value passed to getTea becomes part of how the copy is found, its cache key; The 'use cache' directive shows what goes into it.
lib/supplier.tsNext.jslib/supplier.tsimport { cacheLife } from "next/cache"; const api = process.env.SUPPLIER_API_URL; export type Tea = { id: string; name: string; price: number }; export async function getMenu(): Promise<Tea[]> { "use cache"; cacheLife("hours"); const response = await fetch(`${api}/menu`); return response.json();} export async function getStock(): Promise<{ boxes: number }> { const response = await fetch(`${api}/stock`); return response.json();} export async function getTea(id: string): Promise<Tea> { "use cache"; cacheLife("hours"); const response = await fetch(`${api}/teas/${id}`); return response.json();}- Green tea: $4
- Mint tea: $3
- Lemon tea: $3
In stock today: 12 boxes
Your favorite: Mint tea, $3
Sent with cookie: favorite=mint. The page as the browser shows it when its HTML arrives, drawn with plain styles. Its JavaScript doesn't run here.
getTea is cached on lines 20 and 21. Over the four visits, the supplier was asked for /teas/mint once and /teas/green once: Ben got the copy made for Asha.
getTea("mint").Each row is one answer: what you write, when the work runs, and who gets the result. When a cached result is made again later depends on its cacheLife profile and on where the copy is kept; Cache lifetimes and tags covers both.
| The answer | What you write | When the work runs | Who gets the result |
|---|---|---|---|
| Cache | "use cache" in the function or component, often with cacheLife. | During the build, when it's part of the shell. After that, as its cacheLife profile and the cache store decide. | Everyone: one copy for all visitors. |
| Stream | A <Suspense> around the component that waits. | For every request, after the shell has gone out. | Only the visitor whose request it is. |
| Both | <Suspense> around the part that reads the request, and "use cache" on the work it passes the value to. | At request time, once for each different value. | Everyone who sends the same value. |
What the build reports
With Cache Components on, next build is also a checker: it tries to prerender every route, and tells you about each piece of work that has no answer. Here is the shop with one more file, a Route Handler that answers GET /api/stock with the stock as JSON, for the shop's phone app:
app/api/stock/route.tsimport { getStock } from "@/lib/supplier"; export async function GET() { return Response.json(await getStock());}$ 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 (5/5) Finalizing page optimization ... Route (app) Revalidate Expire┌ ○ /├ ○ /_not-found├ ƒ /api/stock└ ◐ /teas 1h 1d ○ (Static) prerendered as static content◐ (Partial Prerender) prerendered as static HTML with dynamic server-streamed contentƒ (Dynamic) server-rendered on demandLine 4 of app/api/stock/route.ts calls the uncached getStock(). The Terminal tab shows what the build reported, in order:
- Cache Components enabledand- Partial Prefetching enabled: the two options fromnext.config.ts.- The route table, with a symbol for each route. Each row is one symbol, its meaning as the build's legend prints it, and the route that got it here:
| Symbol | The legend says | Why this route got it |
|---|---|---|
○ | prerendered as static content | /: nothing in the home page waits for data or for the request. |
◐ | prerendered as static HTML with dynamic server-streamed content | /teas: the heading and the cached menu are in the shell, and the stock streams in. |
ƒ | server-rendered on demand | /api/stock: a GET Route Handler that fetches uncached data runs for each request. |
A GET Route Handler follows the same rules as a page: Route Handlers showed that caching its work makes it ○. When a build can't finish, it prints an error instead of a table. With Cache Components, these are the errors you'll meet, each one recorded above or below:
- Uncached or request data outside
Suspense. Next.js encountered uncached or runtime data during prerendering, with the fixes[stream],[cache]and[block]. - An unstable value. Next.js encountered the unstable value
new Date()while prerendering, with[dynamic],[cache],[client]and[measure]. - An option from the older model. Route segment config "revalidate" is not compatible with
nextConfig.cacheComponents. Please remove it.
The first two name the route, and the second also points at the line. Both end with a Learn more link to a page on nextjs.org that explains the fixes.
How Cache Components replaced older flags
Cache Components grew out of three experiments, each with its own option. You'll still meet them in older projects and older articles, so here is the path, from Next.js's release posts and docs:
- 2023: Partial Prerendering. Next.js 14 previewed a static shell with streamed holes. Later releases turned it on with
experimental.ppr. - October 2024:
dynamicIOand"use cache". A post called "Our Journey with Caching" introduced an experimental mode on Next.js's canary releases: nothing cached by default, and the new directive to cache, turned on withexperimental.dynamicIO. The docs list"use cache"as experimental from Next.js 15.0.0, and another option,experimental.useCache, turned on the directive by itself. - October 2025: Next.js 16. The three became one stable option,
cacheComponents, moved fromexperimental.cacheComponentsto the top level of the config.experimental.pprand theexperimental_pprexport were removed. - October 2026: Next.js 16.4. Recommended for every app, and on in every new project.
In Next.js 16.4.3, each older option does something different. Each button is the shop with one piece of older code. Before you click, guess which ones build.
next.config.tsimport type { NextConfig } from "next"; const nextConfig: NextConfig = { experimental: { dynamicIO: true, },}; export default nextConfig;$ next build▲ Next.js 16.4.3 (Turbopack) > Build error occurredError: Fatal next config errors found in next.config.ts that must be fixed: Unrecognized key(s) in object: 'dynamicIO' at "experimental" `experimental.dynamicIO` has been replaced by `cacheComponents`. Please update your next.config file accordingly. Learn more: https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponentsThese configuration options are required or have been migrated. Please update your configuration.See more info here: https://nextjs.org/docs/messages/invalid-next-config at ignore-listed framesNext.js stops before it builds anything: experimental.dynamicIO has been replaced by cacheComponents. Please update your next.config file accordingly.
experimental.dynamicIO stops Next.js before it builds anything. experimental.useCache builds with a warning that it's deprecated: it turns on "use cache" alone, without the rest of Cache Components, so the stock was fetched during the build again and the page is ○ /teas. Its replacement is cacheComponents. (Partial Prerendering records what experimental.ppr does now.)
The other three are route segment config: options a page or layout sets by exporting a constant, read by Next.js for that part of the address. dynamic, revalidate and fetchCache set caching for a whole route, and with Cache Components on, each one stops the build. Caching is now chosen piece by piece, in the code that does the work. Each row is one piece of older code and what replaces it:
| Older code | In Next.js 16.4.3 with Cache Components | What to write instead |
|---|---|---|
experimental: { dynamicIO: true } | Next.js 16.4.3 stops at the config file: dynamicIO has been replaced by cacheComponents. | cacheComponents: true, at the top level of the config. |
experimental: { useCache: true } | Deprecated. It still turns on "use cache" by itself, without the rest of Cache Components. | cacheComponents: true. |
experimental: { ppr: true } | Removed in Next.js 16; the build stops. | cacheComponents: true, which makes Partial Prerendering the default. |
export const dynamic = "force-dynamic" | With Cache Components on, the build stops. | Remove it. Uncached work already runs for each request. |
export const revalidate = 3600 | With Cache Components on, the build stops. | "use cache" with cacheLife("hours") on the data or UI that may be an hour old. |
export const fetchCache = "force-cache" | With Cache Components on, the build stops. | "use cache" around the fetches to keep; leave the others outside. |
A project that leaves cacheComponents off still works the older way, with fetch cache options, unstable_cache and route segment config. Caching before Cache Components teaches that model, and how to move a project from it, one route at a time.
Deciding for each part of a page
Planning a page with Cache Components is one question for each part: does it wait for anything, and if so, who can share the answer? A part that waits for nothing needs no decision. Sort these parts of a tea shop page:
- The page's heading and a sentence about the shop.
- The menu: the same for every visitor, changed a few times a year.
- Today's stock, which changes all day.
- What's in this visitor's cart, read from a cookie.
- This visitor's favorite tea, from a cookie, with the tea's details from the supplier.
- Search results for the words in the address, like
?q=mint. - Reviews from the supplier, updated once a day for everyone.
- The number of teas, counted from a list written in the file.
Sort each part of a page by what it needs with Cache Components on.
Start from the data, not from the page: ask how fresh each piece must be, and whether it belongs to one visitor. Then the page follows. When unsure, stream first. A streamed part always shows current data, and you can cache it later, once you know how old it may be. Caching too early can show a visitor yesterday's stock.
Common mix-ups
- "Cache Components caches my components automatically." No. The name says what it lets you do. Nothing that waits for data is cached until you write
"use cache". - "Dynamic by default means every page is made for each request." No. Everything that waits for nothing is still prerendered: the home page is
○ /, and the Teas page's heading and menu are in its shell. - "Slow code fails the build." No. The build reacts to work that waits for data or for the request outside
Suspenseand outside a cache, however fast it is. - "Suspense makes a part dynamic." No.
Suspenseonly gives a waiting part something to show. What makes a part dynamic is uncached work or request data inside it. - "A part that reads a cookie can't use the cache." No. Read the cookie in
Suspense, then pass its value to a cached function. Asha and Ben shared one copy ofgetTea("mint"). - "
export const revalidatestill sets how long a page is kept." Not with Cache Components on: the build stops. Lifetimes belong to cached code, withcacheLife.
Practice exercises
These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.
Which line of this next.config.ts turns on Cache Components?
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
cacheComponents: true,
partialPrefetching: true,
};
export default nextConfig;
Line 5, cacheComponents: true. Line 6 turns on Partial Prefetching, which needs line 5. Without line 5, the recorded build stops at the first "use cache".
This is the Teas page from the overview; getMenu is cached and getStock isn't. Which request did the supplier get during next build?
import { Suspense } from "react";
import { getMenu, getStock } from "@/lib/supplier";
async function Menu() {
const teas = await getMenu();
return (
<ul>
{teas.map((tea) => (
<li key={tea.id}>
{tea.name}: ${tea.price}
</li>
))}
</ul>
);
}
async function Stock() {
const { boxes } = await getStock();
return <p>In stock today: {boxes} boxes</p>;
}
export default function TeasPage() {
return (
<main>
<h1>Our teas</h1>
<Menu />
<Suspense fallback={<p>Checking stock…</p>}>
<Stock />
</Suspense>
</main>
);
}
GET /menu. getMenu is cached, so the build ran it and kept the list in the shell. getStock isn't, so it ran only for the visits: the recording shows /stock twice, both during the visits.
The build stops on this home page with Next.js encountered uncached or runtime data during prerendering. Fix it so the rest of the page is still prerendered. What does the build print for / then?
import { cookies } from "next/headers";
async function Banner() {
const name = (await cookies()).get("name")?.value ?? "friend";
return <p>Welcome back, {name}.</p>;
}
export default function Home() {
return (
<main>
<h1>Tea shop</h1>
<Banner />
</main>
);
}
import { Suspense } from "react";
import { cookies } from "next/headers";
async function Banner() {
const name = (await cookies()).get("name")?.value ?? "friend";
return <p>Welcome back, {name}.</p>;
}
export default function Home() {
return (
<main>
<h1>Tea shop</h1>
<Suspense fallback={<p>Welcome!</p>}>
<Banner />
</Suspense>
</main>
);
}
Put Banner in Suspense. The fallback goes into the shell, and the cookie is read for each request. The recorded build prints ◐ /, and a visit with the cookie name=Asha shows Welcome! first, then Welcome back, Asha.
The home page now counts the teas on the menu with getMenu(), cached for hours. What does the build print for /, with its Revalidate and Expire times?
import { getMenu } from "@/lib/supplier";
export default async function Home() {
const teas = await getMenu();
return (
<main>
<h1>Tea shop</h1>
<p>{teas.length} teas, packed by hand.</p>
</main>
);
}
○ / 1h 1d. The only work is cached, so the build finishes the whole page, and the menu's lifetime gives the route 1h and 1d. The page shows 3 teas, packed by hand.
An older version of the Teas page set export const revalidate = 3600, so the whole page, stock included, could be regenerated at most once an hour. Remove the line and keep that behavior. What does the build print for /teas, with its Revalidate and Expire times?
import { Suspense } from "react";
import { getMenu, getStock } from "@/lib/supplier";
export const revalidate = 3600;
async function Menu() {
const teas = await getMenu();
return (
<ul>
{teas.map((tea) => (
<li key={tea.id}>
{tea.name}: ${tea.price}
</li>
))}
</ul>
);
}
async function Stock() {
const { boxes } = await getStock();
return <p>In stock today: {boxes} boxes</p>;
}
export default function TeasPage() {
return (
<main>
<h1>Our teas</h1>
<Menu />
<Suspense fallback={<p>Checking stock…</p>}>
<Stock />
</Suspense>
</main>
);
}
import { cacheLife } from "next/cache";
const api = process.env.SUPPLIER_API_URL;
export type Tea = { id: string; name: string; price: number };
export async function getMenu(): Promise<Tea[]> {
"use cache";
cacheLife("hours");
const response = await fetch(`${api}/menu`);
return response.json();
}
export async function getStock(): Promise<{ boxes: number }> {
"use cache";
cacheLife("hours");
const response = await fetch(`${api}/stock`);
return response.json();
}
Remove line 4, and cache getStock like getMenu, as lines 15 and 16 of lib/supplier.ts do. Now nothing waits, so the build prints ○ /teas 1h 1d, and the stock was fetched once, during the build, like the old page. If the stock should be fresh instead, keep getStock uncached: the page stays ◐ /teas.
The shop's search page reads q from the address, and search(words) in lib/supplier.ts is cached with "use cache" and cacheLife("hours"). Two visitors search for mint, then one for green. How many times is the supplier asked for mint?
import { Suspense } from "react";
import { search } from "@/lib/supplier";
async function Results({ searchParams }: Pick<PageProps<"/search">, "searchParams">) {
const { q = "" } = await searchParams;
const teas = await search(String(q));
return (
<ul>
{teas.map((tea) => (
<li key={tea.id}>{tea.name}</li>
))}
</ul>
);
}
export default function SearchPage({ searchParams }: PageProps<"/search">) {
return (
<main>
<h1>Search</h1>
<Suspense fallback={<p>Searching…</p>}>
<Results searchParams={searchParams} />
</Suspense>
</main>
);
}
Once. The page uses both: Results reads the address inside Suspense, and search is cached in lib/supplier.ts. The recording shows /search?q=mint once and /search?q=green once for the three visits, and none during the build.
Check your understanding
For each question, ask what the work waits for, and who can share its answer.
Question 1 of 7With Cache Components on, a Server Component awaits an uncached
fetch. When does that fetch run?Choose an answer to see the explanation.
Question 2 of 7This
next.config.tsis whatcreate-next-app16.4.3 writes. What does line 5 turn on?Read the code, then predictTypeScriptimport type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ cacheComponents: true, partialPrefetching: true, }; export default nextConfig;Choose an answer to see the explanation.
Question 3 of 7Why does the recorded build stop on this home page?
Read the code, then predictTSXimport { cookies } from "next/headers"; async function Banner() { const name = (await cookies()).get("name")?.value ?? "friend"; return <p>Welcome back, {name}.</p>; } export default function Home() { return ( <main> <h1>Tea shop</h1> <Banner /> </main> ); }Choose an answer to see the explanation.
Question 4 of 7Asha and Ben both have the cookie
favorite=mint, and Chen hasfavorite=green. WithgetTea(id)cached, how many times is the supplier asked for/teas/mint?Choose an answer to see the explanation.
Question 5 of 7Which of these does Next.js finish during the build without a decision from you?
Choose an answer to see the explanation.
Question 6 of 7This page has
export const revalidate = 3600. What does Next.js 16.4.3 do with it when Cache Components is on?Read the code, then predictTSXimport { Suspense } from "react"; import { getMenu, getStock } from "@/lib/supplier"; export const revalidate = 3600; async function Menu() { const teas = await getMenu(); return ( <ul> {teas.map((tea) => ( <li key={tea.id}> {tea.name}: ${tea.price} </li> ))} </ul> ); } async function Stock() { const { boxes } = await getStock(); return <p>In stock today: {boxes} boxes</p>; } export default function TeasPage() { return ( <main> <h1>Our teas</h1> <Menu /> <Suspense fallback={<p>Checking stock…</p>}> <Stock /> </Suspense> </main> ); }Choose an answer to see the explanation.
Question 7 of 7The home page awaits
getMenu(), which is cached withcacheLife("hours"). What does the build print for/?Read the code, then predictTSXimport { getMenu } from "@/lib/supplier"; export default async function Home() { const teas = await getMenu(); return ( <main> <h1>Tea shop</h1> <p>{teas.length} teas, packed by hand.</p> </main> ); }Choose an answer to see the explanation.
Key takeaways
cacheComponents: trueinnext.config.tsturns on Cache Components:"use cache", Partial Prerendering for every route, and the build's check. New projects have it since Next.js 16.4.- Dynamic by default: work that waits for data or for the request runs for each request unless you cache it. Without Cache Components, data fetched during the build quietly became part of the page.
- Work that waits for nothing, like computing from a list or reading a file synchronously, is prerendered without any decision.
- For each part that waits: cache it when everyone can share it for a while, stream it when it must be fresh or belongs to one visitor, or both when it depends on the request but the work behind it can be shared.
- The build reports the options it saw, a symbol for each route, and an error with fixes for any work without an answer.
experimental.dynamicIO,experimental.useCacheandexperimental.pprbecamecacheComponents. With it on, the route segment configdynamic,revalidateandfetchCachestop the build.
Remember the one-liner.
Fresh unless you cache it: for every part that waits, cache it, stream it, or both.
The error for "use cache" without the option, and the one for experimental.dynamicIO, are both checked when the config is read. The default lifetime of a cached result, and the lifetimes behind cacheLife("hours"), are in defaultConfig, at lines 2346 to 2379 of config-shared.ts, at release v16.4.3. Cache lifetimes and tags reads them.
Coming next: The 'use cache' directive. It puts "use cache" on files, components and functions, and shows what becomes the cache key and what can't be cached.