The default flipped
The App Router default you relied on is inverted: routes are dynamic until you opt into caching. The new model, the static shell, and the configs that now error.
The default you relied on is gone
You already know Partial Prerendering, Suspense streaming, and the App Router render model. Cache Components is that machinery generalized, with one change that reorders everything you know: a route is dynamic by default, and you opt parts of it into caching.
Before: a route was static until a single request read (cookies(), headers(), an uncached fetch) tainted the whole route dynamic.
Now: nothing is cached unless you say so. You mark reusable work with 'use cache', wrap request reads in Suspense, and the static plus cached output becomes the shell.
Same page, both models
Under the old model, the cookies() read below opts the entire route out of static generation. One line, whole-route consequence. That surprise, dynamic rendering you did not ask for, is exactly what Cache Components removes by making the boundary explicit.
// Before Cache Components// Static by default. One dynamic read taints the whole route.export default async function Page() { const posts = await getPosts() // cached fetch → still static const theme = (await cookies()).get('theme') // this alone → whole route dynamic return <Feed posts={posts} theme={theme} />}// With cacheComponents: true// Dynamic by default. You opt parts INTO the shell.export default function Page() { return ( <> <Feed /> {/* cached via 'use cache' → in the shell */} <Suspense fallback={<ThemeSkeleton />}> <Theme /> {/* reads cookies() → streams in */} </Suspense> </> )}Three kinds of content, decided by your code
Every part of a route sorts into one of three groups. You do not label parts with a config value; Next.js reads the code. Synchronous work is static. A 'use cache' scope is cached. A request read is dynamic, so it needs a Suspense boundary.
- Static
Prerendered at build time. Ships from the edge. No data read.
- Cached
Wrapped in use cache. Reused across requests until it revalidates.
- Dynamic
Read per request. Wrapped in Suspense. Streams in after the shell.
There is no more fetch(url, { next: { revalidate } }) as the primary caching mechanism, and no default fetch memoization deciding static vs dynamic. Caching is a property of your function via 'use cache', not of the transport.
The shell is what a prerender can produce
The static shell is the static content plus the cached content: exactly the part Next.js can render without a live request. It commits in one paint. Dynamic parts are not ready, so their fallbacks sit in the shell and the real content streams in.
- Deploy #1284 finished
- 2 new comments on your PR
The header and the stats form the shell. The shell commits at once. The per user notifications stream in behind a skeleton.
You know PPR shipped a static shell with dynamic holes. What is genuinely new here versus experimental.ppr?
Turn it on, and translate the old configs
cacheComponents: true replaces both experimental.ppr and experimental.dynamicIO. The trap: it hard-errors on routes that still export the old segment configs. Translate them, do not just delete them, because each one encodes behavior the route still needs.
// next.config.tsconst nextConfig: NextConfig = { cacheComponents: true, // replaces experimental.ppr AND experimental.dynamicIO} // Any of these left in a route now ERRORS the build:export const dynamic = 'force-dynamic' // delete — every route is dynamic alreadyexport const revalidate = 3600 // translate → cacheLife({ revalidate: 3600 })export const fetchCache = 'force-cache' // translate → 'use cache'dynamic = 'force-dynamic' is the only safe delete, because every route is dynamic by default now. revalidate, fetchCache, and unstable_cache all need real translation (you will do these in Lesson 2). Also note: no Edge runtime and no static export under Cache Components.
You flip cacheComponents: true and a route that previously rendered fine now errors at build. It exports `revalidate = 60` and reads `cookies()` at the top. What is the correct first move?
What you learned
- The default flipped: routes are dynamic until you opt into caching.
- Static + cached = the shell that prerenders and commits in one paint.
- Caching is a property of your function (
'use cache'), not offetch. cacheComponents: trueerrors on leftoverdynamic/revalidate/fetchCacheexports.
Next: the 'use cache' directive in depth, and the migration from unstable_cache, revalidate, and revalidateTag as you knew them.