Caching without Cache Components

Next.js · Advanced · 13 min

Most existing Next.js 13 to 15 apps use this model, and you will maintain or migrate them.

Apps that don't enable cacheComponents use the previous model, which you will meet in most existing codebases.

Routes are static or dynamic as a whole. At build time Next.js tries to prerender each route. Using a runtime API (cookies(), headers(), searchParams, connection()) or an uncached fetch marked cache: 'no-store' makes the entire route dynamic, rendered per request. Otherwise it is static, even if it fetched data at build time.

Caching is configured per request or per route:

  • fetch(url, { cache: 'force-cache' }) caches a request; since Next.js 15 the default is not cached.
  • fetch(url, { next: { revalidate: 3600, tags: ['posts'] } }) caches with a lifetime and tags.
  • unstable_cache(fn, keyParts, { revalidate, tags }) caches non-fetch work like database queries.
  • Route segment config exports in a page or layout: export const revalidate = 60 (ISR: regenerate at most every 60 s), export const dynamic = 'force-dynamic' or 'force-static', dynamicParams, fetchCache.

On-demand invalidation uses the same revalidatePath and revalidateTag(tag, profile) functions.

These segment options are removed when Cache Components is enabled. Migrating means replacing revalidate exports and fetch options with 'use cache' + cacheLife, and wrapping runtime access in Suspense. The official migration guide walks through it.

Example

// Previous model (cacheComponents not enabled)

// app/blog/page.tsx - regenerate this route at most once per hour (ISR)
export const revalidate = 3600

export default async function Blog() {
  const res = await fetch('https://cms.example.com/posts', {
    next: { revalidate: 3600, tags: ['posts'] },     // cached fetch with tags
  })
  const posts: { id: number; title: string }[] = await res.json()
  return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>
}

// lib/users.ts - cache a database call
import { unstable_cache } from 'next/cache'
import { db } from '@/lib/db'
export const getUserCount = unstable_cache(
  async () => db.user.count(),
  ['user-count'],
  { revalidate: 300, tags: ['users'] },
)

// app/account/page.tsx - reading cookies() makes the whole route dynamic
// export const dynamic = 'force-dynamic'   // or make it explicit

Remember

  • Previous model: a route is entirely static or dynamic
  • Cache via fetch options, unstable_cache and segment config like revalidate
  • These options are removed under Cache Components; migrate to 'use cache' + cacheLife

Watch out for

  • Copying export const revalidate into an app with Cache Components enabled, where it is no longer allowed.

Check yourself

  1. In the previous model, a page reads cookies() in one small component. What happens to the route?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: nextjs.org/docs/app/guides/caching-without-cache-components

Builds on

Unlocks

Nothing builds on this yet.