cf.completefrontend
Open lab My progress
THE NEXT.JS FIELD GUIDE

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.

By the end, you can
  • 01
    Explain dynamic by defaultSay which work Next.js finishes during the build, and which it leaves fresh for each request unless you cache it.
  • 02
    Cache, stream, or bothChoose "use cache", Suspense, or both for each part of a page, and predict how often the data source is asked.
  • 03
    Read 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.

The Teas page: a heading, a cached menu, and today's stockNext.js
app/teas/lib/
app/teas/page.tsx
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>  );}
Recorded from Next.js 16.4.3
localhost:3000/teasTea shop
Home
Our teas
  • 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:

What the supplier was asked, and when
Request to the supplierDuring next buildDuring the two visits
GET /menuOnce. getMenu is cached, so the build ran it and kept the list.Never. Both visits got the list the build kept.
GET /stockNever. 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.

A browser window showing the Teas page. Its heading, Our teas, is marked static: prerendered during the build. The three teas, Green tea $4, Mint tea $3 and Lemon tea $3, are marked cached: prerendered during the build and kept for their cache lifetime. The last line, In stock today: 12 boxes, is marked streamed: made for each visit./teasOur teasGreen tea: $4Mint tea: $3Lemon tea: $3In stock today: 12 boxesStaticprerendered during the buildCachedprerendered, kept for its lifetimeStreamedmade for each visit
One page, three kinds of parts. The heading waits for nothing. The menu waits for 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.
Real-life analogyA tea shop's counter

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: getMenu with "use cache" and cacheLife("hours")
In real life: Phoning the warehouse each time a customer asks about stock
In Next.js: Stock, inside Suspense, 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)
The main idea

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:

next.config.ts, as create-next-app writes itTypeScript
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: cacheLife for how long a copy stays fresh, and cacheTag for 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 Suspense and 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:

The same shop, without Cache Components (an error on purpose)Next.js
app/teas/lib/
next.config.ts
import type { NextConfig } from "next"; const nextConfig: NextConfig = {  /* config options here */}; export default nextConfig;
next build failed · Next.js 16.4.3
$ 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 frames

The 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:

The same shop without Cache Components, and without its cacheNext.js
app/teas/lib/
next.config.ts
import type { NextConfig } from "next"; const nextConfig: NextConfig = {  /* config options here */}; export default nextConfig;
Recorded from Next.js 16.4.3
$ 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 content

The 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:

The same Teas page, with and without Cache Components
QuestionWith Cache ComponentsWithout 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 stockFor each visit: twice for two visits, never during the build.Once, during the build. Neither visit asked.
What a visitor sees for the stockToday'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 behaviorCache 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.
Two columns. Left, without Cache Components: next build asks the supplier for the menu and the stock, and visits 1 and 2 get the build's page. Right, with Cache Components: next build asks only for the menu, and visits 1 and 2 each ask for the stock.Without Cache Componentsnext buildasks /menu, /stockvisit 1the build's pagevisit 2the build's page○ /teas: the stock from the buildWith Cache Componentsnext buildasks for /menuvisit 1asks for /stockvisit 2asks for /stock◐ /teas: the stock for each visit
When the supplier was asked, in the two recordings. Without Cache Components, the build fetched the stock once and wrote it into the page. With it, the build fetched only the cached menu, and each visit asked for the stock.

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.

Playground: which work does the build finish?
The home page, with this version of app/page.tsxNext.js
app/teas/lib/
app/page.tsx
const 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>  );}
Recorded from Next.js 16.4.3
$ 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 content
Try it yourself
Counts a list

Line 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.

Every version was built with next build from Next.js 16.4.3. Only app/page.tsx changes. Reset goes back to the first version.

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, params the build doesn't know, and connection(). They don't exist until a request comes in (Request-time APIs).
  • Values that change on every run: new Date(), Math.random() and crypto.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:

The visitor's favorite tea: read for each request, looked up from a cacheNext.js
app/teas/lib/
app/teas/page.tsx
import { 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>  );}
Recorded from Next.js 16.4.3
localhost:3000/teasTea shop
Home
Our teas
  • 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.

Playground: stream only, or stream and cache?
The Teas page, with this version of lib/supplier.tsNext.js
app/teas/lib/
lib/supplier.ts
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 }> {  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();}
Recorded from Next.js 16.4.3
localhost:3000/teasTea shop
Home
Our teas
  • 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.

Try it yourself
Stream and cache

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.

Both versions were built with next build from Next.js 16.4.3 and visited by the same four visitors, against the same stand-in supplier. Only lib/supplier.ts changes. Reset goes back to the first version.
Three windows. Cache: the menu, the same for everyone, with use cache. Stream: today's stock, fresh per visit, with Suspense. Both: your favorite tea, read from the cookie for each visit and shared for each tea, with Suspense and use cache.cacheThe menusame for everyone"use cache"streamToday's stockfresh per visit<Suspense>bothYour favorite teashared per tea<Suspense> + "use cache"
Three answers for work that waits. Cache what everyone can share, stream what must be fresh or belongs to one visitor, and do both when the request picks the value but the work for each value can be shared: Asha and Ben shared one 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.

Cache, stream, or both
The answerWhat you writeWhen the work runsWho 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.
StreamA <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:

The shop with a Route Handler for its phone appNext.js
app/api/stock/teas/lib/
app/api/stock/route.ts
import { getStock } from "@/lib/supplier"; export async function GET() {  return Response.json(await getStock());}
Recorded from Next.js 16.4.3
$ 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 demand

Line 4 of app/api/stock/route.ts calls the uncached getStock(). The Terminal tab shows what the build reported, in order:

  1. - Cache Components enabled and - Partial Prefetching enabled: the two options from next.config.ts.
  2. 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:
The three symbols in this build
SymbolThe legend saysWhy 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: dynamicIO and "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 with experimental.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 from experimental.cacheComponents to the top level of the config. experimental.ppr and the experimental_ppr export 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.

Playground: what does Next.js 16.4.3 do with older code?
The tea shop, with this piece of older codeNext.js
app/teas/lib/
next.config.ts
import type { NextConfig } from "next"; const nextConfig: NextConfig = {  experimental: {    dynamicIO: true,  },}; export default nextConfig;
next build failed · Next.js 16.4.3
$ 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 frames
Try it yourself
experimental.dynamicIO

Next.js stops before it builds anything: experimental.dynamicIO has been replaced by cacheComponents. Please update your next.config file accordingly.

Every version was built with next build from Next.js 16.4.3. The first two change next.config.ts; the other three change app/teas/page.tsx, with Cache Components on. Reset goes back to the first version.

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 options and what replaces them
Older codeIn Next.js 16.4.3 with Cache ComponentsWhat 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 = 3600With 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:

Cache it, stream it, or both?
  • 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.
Try it yourself
0 of 8 correct

Sort each part of a page by what it needs with Cache Components on.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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 Suspense and outside a cache, however fast it is.
  • "Suspense makes a part dynamic." No. Suspense only 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 of getTea("mint").
  • "export const revalidate still sets how long a page is kept." Not with Cache Components on: the build stops. Lifetimes belong to cached code, with cacheLife.

Practice exercises

These exercises are about Next.js projects, which can't run inside this page. Answer first, then use the hints.

Exercise 1 · Warm-upFind the switch

Which line of this next.config.ts turns on Cache Components?

Starter codeTypeScript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
  cacheComponents: true,
  partialPrefetching: true,
};

export default nextConfig;

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upWhat ran during the build?

    This is the Teas page from the overview; getMenu is cached and getStock isn't. Which request did the supplier get during next build?

    Starter codeTSX
    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>
      );
    }
    

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFix the welcome banner

      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?

      Starter codeTSX
      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>
        );
      }
      

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticePredict the home page's line

        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?

        Starter codeTSX
        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>
          );
        }
        

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeMove an hourly page

          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?

          Starter codeTSX
          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>
            );
          }
          

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengePlan the search page

            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?

            Starter codeTSX
            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>
              );
            }
            

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask what the work waits for, and who can share its answer.

              The Cache Components quiz · 7 questionsScore: first tries count
              1. 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.

              2. Question 2 of 7This next.config.ts is what create-next-app 16.4.3 writes. What does line 5 turn on?

                Read the code, then predictTypeScript
                import type { NextConfig } from "next";
                
                const nextConfig: NextConfig = {
                  /* config options here */
                  cacheComponents: true,
                  partialPrefetching: true,
                };
                
                export default nextConfig;
                

                Choose an answer to see the explanation.

              3. Question 3 of 7Why does the recorded build stop on this home page?

                Read the code, then predictTSX
                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>
                  );
                }
                

                Choose an answer to see the explanation.

              4. Question 4 of 7Asha and Ben both have the cookie favorite=mint, and Chen has favorite=green. With getTea(id) cached, how many times is the supplier asked for /teas/mint?

                Choose an answer to see the explanation.

              5. 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.

              6. 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 predictTSX
                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>
                  );
                }
                

                Choose an answer to see the explanation.

              7. Question 7 of 7The home page awaits getMenu(), which is cached with cacheLife("hours"). What does the build print for /?

                Read the code, then predictTSX
                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>
                  );
                }
                

                Choose an answer to see the explanation.

              Key takeaways

              • cacheComponents: true in next.config.ts turns 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.useCache and experimental.ppr became cacheComponents. With it on, the route segment config dynamic, revalidate and fetchCache stop the build.

              Remember the one-liner.
              Fresh unless you cache it: for every part that waits, cache it, stream it, or both.

              Read it in Next.js's own code

              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.

              CompleteFrontend Clear concepts. Working examples.