404 Not Found

404 Not Found


nginx

Cache Components and `useCache`: Next.js 16’s New Caching Model

use cache is the most transformative API in Next.js 16—it elevates caching from a "side effect of data fetching" to a "first-class citizen at the component level."

1. What You'll Learn


2. A True Story of a Systems Architect

(1) Pain Point: Four identical API calls on the same page

While reviewing the TaskFlow Dashboard, Charlie noticed that the four components on the page—<UserAvatar>, <UserGreeting>, <UserStats>, and <UserNotifications>—were each calling fetch('/api/user'). While the cache hit for the same URL worked fine, the database query function getUserFromDB() was called four times. The fetch cache only applies to HTTP requests; it is completely ineffective against internal server function calls.

Question Data
Page-level database calls 4 identical queries
Time per query 200 ms
Total Additional Time 600 ms Wasted
Database QPS Waste 4x

(2) Solution to use cache

Wrap the function in use cache()—this caches any internal function calls, including database queries, calculations, and file reads.

TSX
// app/dashboard/page.tsx
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'

async function getUserData() {
  'use cache'
  cacheTag('user-data')
  cacheLife({ stale: 300, revalidate: 600 })

  const user = await db.user.findUnique({ where: { id: 1 } })  // Execute only once
  return user
}

export default async function DashboardPage() {
  const user = await getUserData()  // The first call executes a database query
  // Subsequent calls will return the cached result directly.

  return (
    <div>
      <UserAvatar user={user} />
      <UserGreeting user={user} />
      <UserStats user={user} />
      <UserNotifications user={user} />
    </div>
  )
}

(3) Revenue

Dimension Traditional Model use cache
Number of database queries 4 1
Additional delay 600 ms 0 ms
Cache Granularity URL Level Function/Component Level
Custom logic cache ❌ Not supported (HTTP fetch only) ✅ Supports any code

3. The use cache() Directive and Content Cache

use cache is a function-level directive—add 'use cache' at the top of a function to indicate that the function’s result should be cached. The cached content is called the Content Cache, a brand-new caching layer introduced in Next.js 16 that is independent of the traditional Data Cache (fetch cache).

100%
graph TB
    subgraph "Next.js 16 Cache System"
        A[Request Memoization<br/>Request-level memory]
        B[Data Cache<br/>fetch cache]
        C[Content Cache<br/>use cache]
        D[Full Route Cache<br/>Full Route Caching]
    end

    A --> E[Within the same request<br/>The same fetch Remove duplicates]
    B --> F[Cross-Request Persistence<br/>force-cache/no-store]
    C --> G[Caching the Results of Arbitrary Functions<br/>tag + life Control]
    D --> H[Page-level HTML cache]

    style C fill:#d4edda
Cache Level Scope Trigger Lifecycle
Request Memoization Single Request Automatic (Same URL) End of Request
Data Cache Cross-Request fetch(options) Configuration-Dependent
Content Cache Cross-Request 'use cache' Directive cacheLife + cacheTag
Full Route Cache Cross-request Build-time / Runtime Revalidate / On-demand

(1) Basic Syntax

TSX
// app/cache-demo/actions.ts
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'

export async function getExpensiveData(id: string) {
  'use cache'
  cacheTag('expensive', `id-${id}`)         // Tagging
  cacheLife({ stale: 60, revalidate: 300 }) // 60 Return within seconds stale,300 Try again in a few seconds

  // All operations within this function are cached.
  const result = await db.query(...)
  return result
}

(2) cacheLife Configuration

Parameter Type Description Example
stale number (seconds) Returns the result directly while the cache is valid; does not trigger a background refresh { stale: 60 }
revalidate number (seconds) Re-execute the function after this time has elapsed { revalidate: 3600 }
expire number (seconds) Cache expires absolutely; force re-fetch { expire: 86400 }

▶ Example: Basic use cache Usage (Difficulty ⭐)

TSX
// app/cache-basic/page.tsx
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'

async function getServerTime() {
  'use cache'
  cacheTag('server-time')
  cacheLife({ stale: 10, revalidate: 30 })

  // Simulate time-consuming operations
  await new Promise(resolve => setTimeout(resolve, 1000))
  return { time: new Date().toISOString(), server: process.env.HOSTNAME ?? 'local' }
}

export default async function CacheBasicPage() {
  const [t1, t2, t3] = await Promise.all([
    getServerTime(),
    getServerTime(),
    getServerTime(),
  ])

  return (
    <div>
      <h1>use cache — Basic</h1>
      <p>Call 1: {t1.time}</p>
      <p>Call 2: {t2.time}</p>
      <p>Call 3: {t3.time}</p>
      <p><em>All three calls returned the same cached result (no duplicate computation)</em></p>
    </div>
  )
}

Output:

TEXT
Call 1: 2026-07-06T10:00:00.000Z
Call 2: 2026-07-06T10:00:00.000Z  ← Milliseconds of cache overlap
Call 3: 2026-07-06T10:00:00.000Z

4. Practical Use of cacheTag and cacheLife

cacheTag() Labels cached content, cacheLife() controls the expiration policy. Combined with revalidateTag(), this enables fine-grained cache control.

(1) Tagging and Categorization Strategy

100%
graph TB
    A[Application Data] --> B[User Data]
    A --> C[Product Data]
    A --> D[Order Data]
    B --> E[tag: user-profile]
    B --> F[tag: user-settings]
    C --> G[tag: products]
    C --> H[tag: product-{id}]
    D --> I[tag: orders]
    D --> J[tag: order-{id}]

    style A fill:#cce5ff
Policy Tag Examples Invalid Operations Scope
Fine-grained product-42 revalidateTag('product-42') Single product only
Medium Grain products revalidateTag('products') All Products
Coarse-grained catalog revalidateTag('catalog') Entire directory

(2) Dynamic Tags

TSX
// app/products/[id]/page.tsx
import { unstable_cacheTag as cacheTag } from 'next/cache'

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id)
  return <ProductView product={product} />
}

async function getProduct(id: string) {
  'use cache'
  cacheTag('products', `product-${id}`)  // Dynamic tags based on parameters
  return fetch(`https://api.example.com/products/${id}`).then(r => r.json())
}

▶ Example: Tiered Caching Strategy (Difficulty: ⭐⭐)

TSX
// app/cache-strategy/page.tsx
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'

async function getUserProfile(userId: number) {
  'use cache'
  cacheTag('users', `user-${userId}`)
  cacheLife({ stale: 120, revalidate: 600 })  // 2 minutes stale,10 minutes revalidate
  return fetch(`https://jsonplaceholder.typicode.com/users/${userId}`).then(r => r.json())
}

async function getUserPosts(userId: number) {
  'use cache'
  cacheTag('posts', `user-posts-${userId}`)
  cacheLife({ stale: 60, revalidate: 300 })
  return fetch(`https://jsonplaceholder.typicode.com/users/${userId}/posts`).then(r => r.json())
}

export default async function CacheStrategyPage() {
  const [profile, posts] = await Promise.all([
    getUserProfile(1),
    getUserPosts(1),
  ])

  return (
    <div>
      <h1>{profile.name}</h1>
      <p>Email: {profile.email}</p>
      <h2>Posts ({posts.length})</h2>
      <ul>{posts.map((p: any) => <li key={p.id}>{p.title}</li>)}</ul>
    </div>
  )
}

▶ Example: Filtering by tag in Server Action doesn't work (Difficulty: ⭐⭐)

TSX
// app/cache-invalidation/page.tsx
import { unstable_cacheTag as cacheTag, revalidateTag } from 'next/cache'

async function getTaskList() {
  'use cache'
  cacheTag('tasks')
  return fetch('https://jsonplaceholder.typicode.com/todos?_limit=5').then(r => r.json())
}

export default async function CacheInvalidationPage() {
  const tasks = await getTaskList()
  return (
    <div>
      <h1>Task List</h1>
      <ul>{tasks.map((t: any) => <li key={t.id}>{t.title}</li>)}</ul>
      <form action={async () => {
        'use server'
        revalidateTag('tasks')  // Click the button to refresh the task list cache immediately
      }}>
        <button type="submit">Refresh Tasks</button>
      </form>
    </div>
  )
}

5. Implicit fetch Caching vs. Explicit use cache Caching

Dimension Implicit fetch cache (Data Cache) Explicit use cache (Content Cache)
Trigger Method fetch(url) Automatic Function Body 'use cache' Instruction
Cached Content HTTP Response Result returned by any function
Use Cases API calls, HTTP requests Database queries, complex calculations, file reading
Tag System next: { tags: [...] } cacheTag() Function
Time Control next: { revalidate: N } cacheLife() Function
Failure Mode revalidateTag() / revalidatePath() revalidateTag() (same label)

(1) When should you use fetch cache, and when should you use use cache?

Scenario Recommended Caching Method Reason
Call External APIs Data Cache (fetch) Native Support for HTTP Semantics
Database Call Content Cache (use cache) Database call is not an HTTP fetch
Complex computations (50 ms+) Content Cache Any function can be cached
Read Files/Configuration Content Cache File operations cannot be performed via fetch
Mixed HTTP + DB Processing Content Cache Caching Entire Blocks of Logic

▶ Example: Combining Two Caches (Difficulty: ⭐⭐⭐)

TSX
// app/cache-combo/page.tsx
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'

// 1. fetch cache: Call External API
async function getExternalPosts() {
  return fetch('https://jsonplaceholder.typicode.com/posts', {
    next: { tags: ['external-posts'], revalidate: 300 }
  }).then(r => r.json())
}

// 2. use cache:Processed data
async function getProcessedPosts() {
  'use cache'
  cacheTag('processed-posts')
  cacheLife({ stale: 60, revalidate: 600 })

  const raw = await getExternalPosts()  // Dependency fetch cache
  return (raw as any[]).map((p: any) => ({
    id: p.id,
    title: p.title.toUpperCase(),
    summary: p.body.slice(0, 100),
  }))
}

export default async function CacheComboPage() {
  const posts = await getProcessedPosts()

  return (
    <div>
      <h1>Processed Posts ({posts.length})</h1>
      <ul>{posts.map((p: any) => (
        <li key={p.id}><strong>{p.title}</strong><p>{p.summary}</p></li>
      ))}</ul>
    </div>
  )
}

6. Complete Example: Caching Architecture for a User Management System

TSX
// app/cache-system/page.tsx — A Complete Caching Architecture
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'

// ======== Data Layer(use cache Explicit Caching) ========
type User = { id: number; name: string; email: string }
type Post = { id: number; title: string; body: string }

async function getUsers(): Promise<User[]> {
  'use cache'
  cacheTag('users')
  cacheLife({ stale: 120, revalidate: 600 })
  return fetch('https://jsonplaceholder.typicode.com/users').then(r => r.json())
}

async function getUserPosts(userId: number): Promise<Post[]> {
  'use cache'
  cacheTag('posts', `user-posts-${userId}`)
  cacheLife({ stale: 60, revalidate: 300 })
  return fetch(`https://jsonplaceholder.typicode.com/users/${userId}/posts`).then(r => r.json())
}

async function getStats(users: User[]) {
  'use cache'
  cacheTag('stats')
  cacheLife({ stale: 300, revalidate: 1800 })
  return {
    totalUsers: users.length,
    avgNameLength: users.reduce((s, u) => s + u.name.length, 0) / users.length,
  }
}

// ======== Page Components ========
export default async function CacheSystemPage() {
  const users = await getUsers()
  const stats = await getStats(users)

  return (
    <div style={{ maxWidth: 900, margin: '0 auto', padding: 24 }}>
      <h1>User Management</h1>
      <StatsCard stats={stats} />
      <div style={{ display: 'grid', gap: 16, marginTop: 24 }}>
        {users.map(user => (
          <UserCard key={user.id} user={user} />
        ))}
      </div>
    </div>
  )
}

// ======== Child component(Independent Cache) ========
async function UserCard({ user }: { user: User }) {
  const posts = await getUserPosts(user.id)
  return (
    <div style={{ border: '1px solid #ddd', borderRadius: 8, padding: 16 }}>
      <h2>{user.name}</h2>
      <p style={{ color: '#666' }}>{user.email}</p>
      <details>
        <summary>Posts ({posts.length})</summary>
        <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
      </details>
    </div>
  )
}

function StatsCard({ stats }: { stats: { totalUsers: number; avgNameLength: number } }) {
  return (
    <div style={{ background: '#f0f4ff', borderRadius: 8, padding: 16, display: 'flex', gap: 32 }}>
      <div><strong>Total Users</strong><p style={{ fontSize: 24 }}>{stats.totalUsers}</p></div>
      <div><strong>Avg Name Length</strong><p style={{ fontSize: 24 }}>{stats.avgNameLength.toFixed(1)}</p></div>
    </div>
  )
}

// ======== Administrative Tasks ========
// app/cache-system/actions.ts
'use server'
import { revalidateTag } from 'next/cache'

export async function refreshUserData() {
  revalidateTag('users')        // Refresh the user list
}

export async function refreshPosts(userId: number) {
  revalidateTag(`user-posts-${userId}`)  // Refresh a specific user's posts
}

export async function refreshAll() {
  revalidateTag('users')
  revalidateTag('posts')
  revalidateTag('stats')
}

// app/cache-system/admin-button.tsx
'use client'
export function AdminControls() {
  return (
    <div style={{ display: 'flex', gap: 8, margin: '16px 0' }}>
      <form action={async () => {
        const { refreshUserData } = await import('./actions')
        await refreshUserData()
      }}>
        <button type="submit">Refresh Users</button>
      </form>
      <form action={async () => {
        const { refreshAll } = await import('./actions')
        await refreshAll()
      }}>
        <button type="submit">Refresh All</button>
      </form>
    </div>
  )
}

▶ Example: Verifying a Cache Hit (Difficulty: ⭐⭐)

TSX
// app/cache-verify/page.tsx — Check whether the cache hit
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache'

let callCount = 0

async function getUniqueId() {
  'use cache'
  cacheTag('unique-id')
  cacheLife({ revalidate: 30 })
  callCount++
  return { id: crypto.randomUUID(), calls: callCount }
}

export default async function CacheVerifyPage() {
  const [a, b, c] = await Promise.all([
    getUniqueId(),
    getUniqueId(),
    getUniqueId(),
  ])

  return (
    <div>
      <h1>Cache Verification</h1>
      <p>Result A: {a.id} (call #{a.calls})</p>
      <p>Result B: {b.id} (call #{b.calls})</p>
      <p>Result C: {c.id} (call #{c.calls})</p>
      <p><strong>Same ID + calls = 1 → cache hit</strong></p>
    </div>
  )
}

Output:

TEXT
Result A: 550e8400-e29b-41d4-a716-446655440000 (call #1)
Result B: 550e8400-e29b-41d4-a716-446655440000 (call #1)  ← Cache Hit
Result C: 550e8400-e29b-41d4-a716-446655440000 (call #1)  ← Cache Hit

❓ FAQ

Q What is the difference between use cache and useMemo?
A useMemo is a client-side hook that caches computations only within the browser and is scoped to a single render. use cache is a server-side directive that persists the cache across requests, supports tags and expiration times, and allows Server Actions to invalidate it on demand. The two have completely different use cases.
Q Can the same tag be used for both fetch and use cache?
A Yes. revalidateTag('products') will clear both the entry labeled next: { tags: ['products'] } from the fetch data cache and the entry labeled cacheTag('products') from the content cache. This is the key mechanism for achieving unified cache invalidation.
Q Can use cache be used in a client component?
A No. The 'use cache' directive is only valid in server components or server functions. In client components, you should use useMemo or client-side caching solutions such as React Query.
Q What does the unstable_ prefix in unstable_cacheLife and unstable_cacheTag mean?
A It means the API is still under development and may change in future versions. They are available in Next.js 16.2, but we recommend keeping an eye on official updates. The unstable_ prefix is typically removed after 1–2 major releases (expected in Next.js 17 or 18 stable).
Q Where is the Content Cache data stored?
A By default, the Content Cache is stored in memory (persisted across requests in production). When deployed on Vercel, it uses edge storage. In self-hosted environments, it is stored in the file system or in memory. The cache size is limited by the server’s memory; a large cache will consume memory resources.

📖 Summary


📝 Exercises

  1. Basic Problem (⭐): Create a app/cache-demo/page.tsx and use a 'use cache' function to wrap a simulated time-consuming operation (await new Promise(resolve => setTimeout(resolve, 2000))). Call it three times on the page and verify that the second call starts with zero delay (cache hit).

  2. Advanced Exercise (⭐⭐): Build a page that includes a list of users and a list of user articles. Use cacheLife({ revalidate: 300 }) for the user list and cacheLife({ revalidate: 60 }) for the article list. Add two Server Action buttons at the bottom of the page to refresh the user tab and the article tab, respectively. Verify that the tabs function independently.

  3. Challenge (⭐⭐⭐): Implement a "Data Analytics Dashboard" that retrieves data from three different sources (HTTP API + simulated database + calculation functions) and uses use cache for centralized caching. Each data source has a different cacheLife strategy. Add a "Force Refresh All" button that calls revalidateTag to refresh all dashboards simultaneously. Build a tool to track cache hit rates.

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏