Caching

Learn how to cache data and UI in Next.js

This page covers caching with Cache Components, enabled by setting cacheComponents: true in your next.config.ts file. If you're not using Cache Components, see the Caching and Revalidating (Previous Model) guide.

Caching is a technique for storing the result of data fetching and other computations so that future requests for the same data can be served faster, without doing the work again.

Enabling Cache Components

You can enable Cache Components by adding the cacheComponents option to your Next config file:

import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  cacheComponents: true,
}
 
export default nextConfig

Good to know: When Cache Components is enabled, GET Route Handlers follow the same prerendering model as pages. See Route Handlers with Cache Components for details.

Usage

The use cache directive caches the return value of async functions and components. You can apply it at two levels:

  • Data-level: Cache a function that fetches or computes data (e.g., getProducts(), getUser(id))
  • UI-level: Cache an entire component or page (e.g., async function BlogPosts())

Arguments and any closed-over values from parent scopes automatically become part of the cache key, which means different inputs will produce separate cache entries. This enables personalized or parameterized cached content. See serialization requirements and constraints for details on what can be cached and how arguments work.

Data-level caching

To cache an asynchronous function that fetches data, add the use cache directive at the top of the function body:

app/lib/data.ts
import { cacheLife } from 'next/cache'
 
export async function getUsers() {
  'use cache'
  cacheLife('hours')
  return db.query('SELECT * FROM users')
}

Data-level caching is useful when the same data is used across multiple components, or when you want to cache the data independently from the UI.

UI-level caching

To cache an entire component, page, or layout, add the use cache directive at the top of the component or page body:

app/page.tsx
import { cacheLife } from 'next/cache'
 
export default async function Page() {
  'use cache'
  cacheLife('hours')
 
  const users = await db.query('SELECT * FROM users')
 
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

If you add "use cache" at the top of a file, all exported functions in the file will be cached.

Streaming uncached data

For components that fetch data from an asynchronous source such as an API, a database, or any other async operation, and require fresh data on every request, do not use "use cache".

Instead, wrap the component in <Suspense> and provide a fallback UI. At request time, React renders the fallback first, then streams in the resolved content once the async work completes.

page.tsx
import { Suspense } from 'react'
 
async function LatestPosts() {
  const data = await fetch('https://api.example.com/posts')
  const posts = await data.json()
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}
 
export default function Page() {
  return (
    <>
      <h1>My Blog</h1>
      <Suspense fallback={<p>Loading posts...</p>}>
        <LatestPosts />
      </Suspense>
    </>
  )
}

The fallback (<p>Loading posts...</p>) is included in the static shell, while the component's content streams in at request time.

Working with runtime APIs

Request-time APIs require information that is only available when a user makes a request. These include:

Components that access runtime APIs should be wrapped in <Suspense>:

page.tsx
import { cookies } from 'next/headers'
import { Suspense } from 'react'
 
async function UserGreeting() {
  const cookieStore = await cookies()
  const theme = cookieStore.get('theme')?.value || 'light'
  return <p>Your theme: {theme}</p>
}
 
export default function Page() {
  return (
    <>
      <h1>Dashboard</h1>
      <Suspense fallback={<p>Loading...</p>}>
        <UserGreeting />
      </Suspense>
    </>
  )
}

Passing runtime values to cached functions

You can extract values from runtime APIs and pass them as arguments to cached functions:

app/profile/page.tsx
import { cookies } from 'next/headers'
import { Suspense } from 'react'
 
export default function Page() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <ProfileContent />
    </Suspense>
  )
}
 
// Component (not cached) reads runtime data
async function ProfileContent() {
  const session = (await cookies()).get('session')?.value
  return <CachedContent sessionId={session} />
}
 
// Cached component receives extracted value as a prop
async function CachedContent({ sessionId }: { sessionId: string }) {
  'use cache'
  // sessionId becomes part of the cache key
  const data = await fetchUserData(sessionId)
  return <div>{data}</div>
}

At request time, CachedContent executes if no matching cache entry is found, and stores the result for future requests with the same sessionId.

Working with non-deterministic operations

Operations like Math.random(), Date.now(), or crypto.randomUUID() produce different values each time they execute. Cache Components requires you to explicitly handle these.

To generate unique values per request, defer to request time by calling connection() before these operations, and wrap the component in <Suspense>:

page.tsx
import { connection } from 'next/server'
import { Suspense } from 'react'
 
async function UniqueContent() {
  await connection()
  const uuid = crypto.randomUUID()
  return <p>Request ID: {uuid}</p>
}
 
export default function Page() {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <UniqueContent />
    </Suspense>
  )
}

Alternatively, you can cache the result so all users see the same value until revalidation:

page.tsx
export default async function Page() {
  'use cache'
  const buildId = crypto.randomUUID()
  return <p>Build ID: {buildId}</p>
}

Working with deterministic operations

Operations like synchronous I/O, module imports, and pure computations can complete during prerendering. Components using only these operations have their rendered output automatically included in the static HTML shell.

page.tsx
import fs from 'node:fs'
 
export default async function Page() {
  const content = fs.readFileSync('./config.json', 'utf-8')
  const constants = await import('./constants.json')
  const processed = JSON.parse(content).items.map((item) => item.value * 2)
 
  return (
    <div>
      <h1>{constants.appName}</h1>
      <ul>
        {processed.map((value, i) => (
          <li key={i}>{value}</li>
        ))}
      </ul>
    </div>
  )
}

How rendering works

At build time, Next.js renders your route's component tree. How each component is handled depends on the APIs it uses:

  • use cache: the result is cached and included in the static shell
  • <Suspense>: fallback UI is included in the static shell while the content streams at request time
  • Deterministic operations: like pure computations and module imports are automatically included in the static shell

This generates a static shell consisting of HTML for initial page loads and a serialized RSC Payload for client-side navigation, ensuring the browser receives fully rendered content instantly whether users navigate directly to the URL or transition from another page.

Partially re-rendered Product Page showing static nav and product information, and dynamic cart and recommended products

This rendering approach is called Partial Prerendering (PPR), and it's the default behavior with Cache Components.

You can verify that a route was fully prerendered by checking the build output summary. Alternatively, see what content was added to the static shell of any page by viewing the page source in your browser.

Diagram showing partially rendered page on the client, with loading UI for chunks that are being streamed.

Next.js requires you to explicitly handle components that can't complete during prerendering. If they aren't wrapped in <Suspense> or marked with use cache, you'll see an Uncached data was accessed outside of <Suspense> error during development and build time.

🎥 Watch: Why Partial Prerendering and how it works → YouTube (10 minutes).

Opting out of the static shell

Placing a <Suspense> boundary with an empty fallback above the document body in your Root Layout causes the entire app to defer to request time. Because the fallback is empty, there is no static shell to send immediately, so every request blocks until the page is fully rendered. To limit this to specific routes, use multiple root layouts.

app/layout.tsx
import { Suspense } from 'react'
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html>
      <Suspense fallback={null}>
        <body>{children}</body>
      </Suspense>
    </html>
  )
}

Good to know: This same pattern applies when generateViewport accesses uncached dynamic data. See Viewport with Cache Components for a detailed example.

Putting it all together

Here's a complete example showing static content, cached dynamic content, and streaming dynamic content working together on a single page:

app/blog/page.tsx
import { Suspense } from 'react'
import { cookies } from 'next/headers'
import { cacheLife, cacheTag, updateTag } from 'next/cache'
import Link from 'next/link'
 
export default function BlogPage() {
  return (
    <>
      {/* Static content - prerendered automatically */}
      <header>
        <h1>Our Blog</h1>
        <nav>
          <Link href="/">Home</Link> | <Link href="/about">About</Link>
        </nav>
      </header>
 
      {/* Cached dynamic content - included in the static shell */}
      <BlogPosts />
 
      {/* Runtime dynamic content - streams at request time */}
      <Suspense fallback={<p>Loading your preferences...</p>}>
        <UserPreferences />
      </Suspense>
 
      {/* Mutation - server action that revalidates the cache */}
      <Suspense fallback={<p>Loading...</p>}>
        <CreatePost />
      </Suspense>
    </>
  )
}
 
// Everyone sees the same blog posts (revalidated every hour)
async function BlogPosts() {
  'use cache'
  cacheLife('hours')
  cacheTag('posts')
 
  const res = await fetch('https://api.vercel.app/blog')
  const posts = await res.json()
 
  return (
    <section>
      <h2>Latest Posts</h2>
      <ul>
        {posts.slice(0, 5).map((post: any) => (
          <li key={post.id}>
            <h3>{post.title}</h3>
            <p>
              By {post.author} on {post.date}
            </p>
          </li>
        ))}
      </ul>
    </section>
  )
}
 
// Personalized per user based on their cookie
async function UserPreferences() {
  const theme = (await cookies()).get('theme')?.value || 'light'
  const favoriteCategory = (await cookies()).get('category')?.value
 
  return (
    <aside>
      <p>Your theme: {theme}</p>
      {favoriteCategory && <p>Favorite category: {favoriteCategory}</p>}
    </aside>
  )
}
 
// Admin-only form that creates a post and revalidates the cache
async function CreatePost() {
  const isAdmin = (await cookies()).get('role')?.value === 'admin'
  if (!isAdmin) return null
 
  async function createPost(formData: FormData) {
    'use server'
    await db.post.create({ data: { title: formData.get('title') } })
    updateTag('posts')
  }
 
  return (
    <form action={createPost}>
      <input name="title" placeholder="Post title" required />
      <button type="submit">Publish</button>
    </form>
  )
}

During prerendering, the header (static) and blog posts (cached with use cache) become part of the static shell along with the fallback UI for user preferences. Only the personalized preferences stream in at request time. When an admin publishes a new post, the updateTag call immediately expires the blog posts cache so the next visitor sees it.

Good to know: generateMetadata and generateViewport track runtime data access separately from the page. See Metadata with Cache Components and Viewport with Cache Components for how to handle this.