next-cache-components

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Cache Components (Next.js 16+)

缓存组件(Next.js 16+)

Cache Components enable Partial Prerendering (PPR) - mix static, cached, and dynamic content in a single route.
缓存组件支持部分预渲染(PPR)——在单个路由中混合静态、缓存和动态内容。

Enable Cache Components

启用缓存组件

ts
// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig
This replaces the old
experimental.ppr
flag.

ts
// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig
这将替代旧的
experimental.ppr
标志。

Three Content Types

三种内容类型

With Cache Components enabled, content falls into three categories:
启用缓存组件后,内容分为三类:

1. Static (Auto-Prerendered)

1. 静态(自动预渲染)

Synchronous code, imports, pure computations - prerendered at build time:
tsx
export default function Page() {
  return (
    <header>
      <h1>Our Blog</h1>  {/* Static - instant */}
      <nav>...</nav>
    </header>
  )
}
同步代码、导入、纯计算逻辑——在构建时预渲染:
tsx
export default function Page() {
  return (
    <header>
      <h1>我们的博客</h1>  {/* 静态内容——即时加载 */}
      <nav>...</nav>
    </header>
  )
}

2. Cached (
use cache
)

2. 缓存(
use cache

Async data that doesn't need fresh fetches every request:
tsx
async function BlogPosts() {
  'use cache'
  cacheLife('hours')

  const posts = await db.posts.findMany()
  return <PostList posts={posts} />
}
不需要每次请求都获取最新数据的异步内容:
tsx
async function BlogPosts() {
  'use cache'
  cacheLife('hours')

  const posts = await db.posts.findMany()
  return <PostList posts={posts} />
}

3. Dynamic (Suspense)

3. 动态(Suspense 包裹)

Runtime data that must be fresh - wrap in Suspense:
tsx
import { Suspense } from 'react'

export default function Page() {
  return (
    <>
      <BlogPosts />  {/* Cached */}

      <Suspense fallback={<p>Loading...</p>}>
        <UserPreferences />  {/* Dynamic - streams in */}
      </Suspense>
    </>
  )
}

async function UserPreferences() {
  const theme = (await cookies()).get('theme')?.value
  return <p>Theme: {theme}</p>
}

必须保持最新的运行时数据——用 Suspense 包裹:
tsx
import { Suspense } from 'react'

export default function Page() {
  return (
    <>
      <BlogPosts />  {/* 缓存内容 */}

      <Suspense fallback={<p>加载中...</p>}>
        <UserPreferences />  {/* 动态内容——流式加载 */}
      </Suspense>
    </>
  )
}

async function UserPreferences() {
  const theme = (await cookies()).get('theme')?.value
  return <p>主题:{theme}</p>
}

use cache
Directive

use cache
指令

File Level

文件级别

tsx
'use cache'

export default async function Page() {
  // Entire page is cached
  const data = await fetchData()
  return <div>{data}</div>
}
tsx
'use cache'

export default async function Page() {
  // 整个页面都被缓存
  const data = await fetchData()
  return <div>{data}</div>
}

Component Level

组件级别

tsx
export async function CachedComponent() {
  'use cache'
  const data = await fetchData()
  return <div>{data}</div>
}
tsx
export async function CachedComponent() {
  'use cache'
  const data = await fetchData()
  return <div>{data}</div>
}

Function Level

函数级别

tsx
export async function getData() {
  'use cache'
  return db.query('SELECT * FROM posts')
}

tsx
export async function getData() {
  'use cache'
  return db.query('SELECT * FROM posts')
}

Cache Profiles

缓存配置文件

Built-in Profiles

内置配置文件

tsx
'use cache'                    // Default: 5m stale, 15m revalidate
tsx
'use cache: remote'           // Platform-provided cache (Redis, KV)
tsx
'use cache: private'          // For compliance, allows runtime APIs
tsx
'use cache'                    // 默认:5分钟过期,15分钟后台重新验证
tsx
'use cache: remote'           // 平台提供的缓存(Redis、KV)
tsx
'use cache: private'          // 用于合规场景,允许使用运行时API

cacheLife()
- Custom Lifetime

cacheLife()
- 自定义生命周期

tsx
import { cacheLife } from 'next/cache'

async function getData() {
  'use cache'
  cacheLife('hours')  // Built-in profile
  return fetch('/api/data')
}
Built-in profiles:
'default'
,
'minutes'
,
'hours'
,
'days'
,
'weeks'
,
'max'
tsx
import { cacheLife } from 'next/cache'

async function getData() {
  'use cache'
  cacheLife('hours')  // 使用内置配置文件
  return fetch('/api/data')
}
内置配置文件:
'default'
'minutes'
'hours'
'days'
'weeks'
'max'

Inline Configuration

内联配置

tsx
async function getData() {
  'use cache'
  cacheLife({
    stale: 3600,      // 1 hour - serve stale while revalidating
    revalidate: 7200, // 2 hours - background revalidation interval
    expire: 86400,    // 1 day - hard expiration
  })
  return fetch('/api/data')
}

tsx
async function getData() {
  'use cache'
  cacheLife({
    stale: 3600,      // 1小时——返回过期内容同时后台重新验证
    revalidate: 7200, // 2小时——后台重新验证间隔
    expire: 86400,    // 1天——强制过期时间
  })
  return fetch('/api/data')
}

Cache Invalidation

缓存失效

cacheTag()
- Tag Cached Content

cacheTag()
- 为缓存内容打标签

tsx
import { cacheTag } from 'next/cache'

async function getProducts() {
  'use cache'
  cacheTag('products')
  return db.products.findMany()
}

async function getProduct(id: string) {
  'use cache'
  cacheTag('products', `product-${id}`)
  return db.products.findUnique({ where: { id } })
}
tsx
import { cacheTag } from 'next/cache'

async function getProducts() {
  'use cache'
  cacheTag('products')
  return db.products.findMany()
}

async function getProduct(id: string) {
  'use cache'
  cacheTag('products', `product-${id}`)
  return db.products.findUnique({ where: { id } })
}

updateTag()
- Immediate Invalidation

updateTag()
- 即时失效

Use when you need the cache refreshed within the same request:
tsx
'use server'

import { updateTag } from 'next/cache'

export async function updateProduct(id: string, data: FormData) {
  await db.products.update({ where: { id }, data })
  updateTag(`product-${id}`)  // Immediate - same request sees fresh data
}
当你需要在同一个请求中刷新缓存时使用:
tsx
'use server'

import { updateTag } from 'next/cache'

export async function updateProduct(id: string, data: FormData) {
  await db.products.update({ where: { id }, data })
  updateTag(`product-${id}`)  // 即时生效——同一请求中可获取最新数据
}

revalidateTag()
- Background Revalidation

revalidateTag()
- 后台重新验证

Use for stale-while-revalidate behavior:
tsx
'use server'

import { revalidateTag } from 'next/cache'

export async function createPost(data: FormData) {
  await db.posts.create({ data })
  revalidateTag('posts')  // Background - next request sees fresh data
}

用于实现“返回过期内容同时后台更新”的行为:
tsx
'use server'

import { revalidateTag } from 'next/cache'

export async function createPost(data: FormData) {
  await db.posts.create({ data })
  revalidateTag('posts')  // 后台执行——下一次请求将获取最新数据
}

Runtime Data Constraint

运行时数据限制

Cannot access
cookies()
,
headers()
, or
searchParams
inside
use cache
.
use cache
内部无法访问
cookies()
headers()
searchParams

Solution: Pass as Arguments

解决方案:作为参数传入

tsx
// Wrong - runtime API inside use cache
async function CachedProfile() {
  'use cache'
  const session = (await cookies()).get('session')?.value  // Error!
  return <div>{session}</div>
}

// Correct - extract outside, pass as argument
async function ProfilePage() {
  const session = (await cookies()).get('session')?.value
  return <CachedProfile sessionId={session} />
}

async function CachedProfile({ sessionId }: { sessionId: string }) {
  'use cache'
  // sessionId becomes part of cache key automatically
  const data = await fetchUserData(sessionId)
  return <div>{data.name}</div>
}
tsx
// 错误示例——在use cache内部使用运行时API
async function CachedProfile() {
  'use cache'
  const session = (await cookies()).get('session')?.value  // 报错!
  return <div>{session}</div>
}

// 正确示例——在外部提取,作为参数传入
async function ProfilePage() {
  const session = (await cookies()).get('session')?.value
  return <CachedProfile sessionId={session} />
}

async function CachedProfile({ sessionId }: { sessionId: string }) {
  'use cache'
  // sessionId会自动成为缓存键的一部分
  const data = await fetchUserData(sessionId)
  return <div>{data.name}</div>
}

Exception:
use cache: private

例外情况:
use cache: private

For compliance requirements when you can't refactor:
tsx
async function getData() {
  'use cache: private'
  const session = (await cookies()).get('session')?.value  // Allowed
  return fetchData(session)
}

当你无法重构代码,且有合规要求时使用:
tsx
async function getData() {
  'use cache: private'
  const session = (await cookies()).get('session')?.value  // 允许使用
  return fetchData(session)
}

Cache Key Generation

缓存键生成

Cache keys are automatic based on:
  • Build ID - invalidates all caches on deploy
  • Function ID - hash of function location
  • Serializable arguments - props become part of key
  • Closure variables - outer scope values included
tsx
async function Component({ userId }: { userId: string }) {
  const getData = async (filter: string) => {
    'use cache'
    // Cache key = userId (closure) + filter (argument)
    return fetch(`/api/users/${userId}?filter=${filter}`)
  }
  return getData('active')
}

缓存键会自动基于以下内容生成:
  • 构建ID——部署时使所有缓存失效
  • 函数ID——函数位置的哈希值
  • 可序列化参数——props会成为键的一部分
  • 闭包变量——包含外部作用域的值
tsx
async function Component({ userId }: { userId: string }) {
  const getData = async (filter: string) => {
    'use cache'
    // 缓存键 = userId(闭包) + filter(参数)
    return fetch(`/api/users/${userId}?filter=${filter}`)
  }
  return getData('active')
}

Complete Example

完整示例

tsx
import { Suspense } from 'react'
import { cookies } from 'next/headers'
import { cacheLife, cacheTag } from 'next/cache'

export default function DashboardPage() {
  return (
    <>
      {/* Static shell - instant from CDN */}
      <header><h1>Dashboard</h1></header>
      <nav>...</nav>

      {/* Cached - fast, revalidates hourly */}
      <Stats />

      {/* Dynamic - streams in with fresh data */}
      <Suspense fallback={<NotificationsSkeleton />}>
        <Notifications />
      </Suspense>
    </>
  )
}

async function Stats() {
  'use cache'
  cacheLife('hours')
  cacheTag('dashboard-stats')

  const stats = await db.stats.aggregate()
  return <StatsDisplay stats={stats} />
}

async function Notifications() {
  const userId = (await cookies()).get('userId')?.value
  const notifications = await db.notifications.findMany({
    where: { userId, read: false }
  })
  return <NotificationList items={notifications} />
}

tsx
import { Suspense } from 'react'
import { cookies } from 'next/headers'
import { cacheLife, cacheTag } from 'next/cache'

export default function DashboardPage() {
  return (
    <>
      {/* 静态外壳——从CDN即时加载 */}
      <header><h1>仪表盘</h1></header>
      <nav>...</nav>

      {/* 缓存内容——加载快,每小时重新验证 */}
      <Stats />

      {/* 动态内容——流式加载最新数据 */}
      <Suspense fallback={<NotificationsSkeleton />}>
        <Notifications />
      </Suspense>
    </>
  )
}

async function Stats() {
  'use cache'
  cacheLife('hours')
  cacheTag('dashboard-stats')

  const stats = await db.stats.aggregate()
  return <StatsDisplay stats={stats} />
}

async function Notifications() {
  const userId = (await cookies()).get('userId')?.value
  const notifications = await db.notifications.findMany({
    where: { userId, read: false }
  })
  return <NotificationList items={notifications} />
}

Migration from Previous Versions

从旧版本迁移

Old ConfigReplacement
experimental.ppr
cacheComponents: true
dynamic = 'force-dynamic'
Remove (default behavior)
dynamic = 'force-static'
'use cache'
+
cacheLife('max')
revalidate = N
cacheLife({ revalidate: N })
unstable_cache()
'use cache'
directive

旧配置替代方案
experimental.ppr
cacheComponents: true
dynamic = 'force-dynamic'
移除(默认行为)
dynamic = 'force-static'
'use cache'
+
cacheLife('max')
revalidate = N
cacheLife({ revalidate: N })
unstable_cache()
'use cache'
指令

Limitations

限制

  • Edge runtime not supported - requires Node.js
  • Static export not supported - needs server
  • Non-deterministic values (
    Math.random()
    ,
    Date.now()
    ) execute once at build time inside
    use cache
For request-time randomness outside cache:
tsx
import { connection } from 'next/server'

async function DynamicContent() {
  await connection()  // Defer to request time
  const id = crypto.randomUUID()  // Different per request
  return <div>{id}</div>
}
Sources:
  • 不支持Edge运行时——需要Node.js环境
  • 不支持静态导出——需要服务器
  • 非确定性值
    Math.random()
    Date.now()
    )在
    use cache
    内部会在构建时执行一次
如需在缓存外实现请求级别的随机性:
tsx
import { connection } from 'next/server'

async function DynamicContent() {
  await connection()  // 延迟到请求时执行
  const id = crypto.randomUUID()  // 每次请求都不同
  return <div>{id}</div>
}
资料来源: