Next.js

缓存组件

深入理解 Next.js 缓存组件的特性,包括 use cache 指令、缓存生命周期管理、缓存标签和失效策略

Cache Components 是 Next.js 16 引入的一项重要特性,允许在单个路由中混合静态、缓存和动态内容,兼具静态站点的速度和动态渲染的灵活性。

核心概念

传统的服务端渲染应用通常需要在静态页面(快速但内容陈旧)和动态页面(内容新鲜但速度慢)之间做出选择。将工作转移到客户端虽然减轻了服务器负载,但会增加打包体积并降低初始渲染速度。

Cache Components 通过将路由预渲染为静态 HTML 外壳来消除这些权衡,该外壳立即发送到浏览器,动态内容在准备就绪时更新 UI。

渲染工作流程

在构建时,Next.js 渲染路由的组件树。只要组件不访问网络资源、某些系统 API 或不需要传入请求来渲染,它们的输出就会自动添加到静态外壳中。否则,你必须选择如何处理它们:

  1. 延迟渲染:使用 React 的 <Suspense> 包装组件,在内容准备就绪之前显示后备 UI
  2. 缓存结果:使用 use cache 指令将结果包含在静态外壳中(如果不需要请求数据)

启用 Cache Components

next.config.ts 中设置 cacheComponents 标志:

import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  cacheComponents: true,
}

export default nextConfig
/** @type {import('next').NextConfig} */
const nextConfig = {
  cacheComponents: true,
}

module.exports = nextConfig

use cache 指令

use cache 指令允许标记路由、React 组件或函数为可缓存。可以在文件顶部使用以指示文件中的所有导出都应被缓存,或在函数或组件顶部内联使用以缓存返回值。

基本用法

// 文件级别
'use cache'

export default async function Page() {
  const data = await fetch('/api/data')
  return <div>{data}</div>
}
// 组件级别
export async function MyComponent() {
  'use cache'
  const data = await fetch('/api/data')
  return <div>{data}</div>
}
// 函数级别
export async function getData() {
  'use cache'
  const data = await fetch('/api/data')
  return data
}

延迟渲染到请求时

使用 <Suspense> 包装动态组件,在内容准备就绪之前显示后备 UI:

import { Suspense } from 'react'

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params

  return (
    <div>
      <ProductDetails id={id} />
      <Suspense fallback={<div>Loading recommendations...</div>}>
        <Recommendations productId={id} />
      </Suspense>
    </div>
  )
}

async function ProductDetails({ id }: { id: string }) {
  'use cache'
  const product = await fetchProduct(id)
  return <div>{product.name}</div>
}

async function Recommendations({ productId }: { productId: string }) {
  const recommendations = await fetchRecommendations(productId)
  return (
    <div>
      {recommendations.map((rec) => (
        <ProductCard key={rec.id} product={rec} />
      ))}
    </div>
  )
}

传递参数

use cache 可以接受参数,缓存键基于参数值:

async function getUser(id: string) {
  'use cache'
  const user = await db.user.findUnique({ where: { id } })
  return user
}

// 每个 ID 都会创建单独的缓存条目
const user1 = await getUser('1')
const user2 = await getUser('2')

限制

use cache 作用域内不能使用以下 API:

  • cookies()
  • headers()
  • searchParams
  • connection()

如果需要使用这些 API,请使用 use cache: privateuse cache: remote

cacheLife 函数

cacheLife 函数用于设置函数或组件的缓存生命周期,必须与 use cache 指令一起使用。

预设配置文件

Next.js 提供了涵盖常见缓存需求的预设缓存配置文件:

import { cacheLife } from 'next/cache'

async function getStockPrice(symbol: string) {
  'use cache'
  cacheLife('seconds') // 实时数据(股票价格、实时比分)
  const price = await fetchStockPrice(symbol)
  return price
}

async function getNewsFeed() {
  'use cache'
  cacheLife('minutes') // 频繁更新(社交动态、新闻)
  const news = await fetchNews()
  return news
}

async function getProductCatalog() {
  'use cache'
  cacheLife('hours') // 每日多次更新(产品目录)
  const products = await fetchProducts()
  return products
}

async function getBlogPosts() {
  'use cache'
  cacheLife('days') // 每日更新(博客文章)
  const posts = await fetchPosts()
  return posts
}

async function getDocumentation() {
  'use cache'
  cacheLife('weeks') // 每周更新(文档)
  const docs = await fetchDocs()
  return docs
}

async function getTermsOfService() {
  'use cache'
  cacheLife('max') // 很少更新(服务条款)
  const terms = await fetchTerms()
  return terms
}

自定义缓存配置

可以创建自定义缓存配置以满足特定需求:

import { cacheLife } from 'next/cache'

async function getData() {
  'use cache'
  cacheLife({
    stale: 3600,      // 客户端认为数据新鲜的时间(秒)
    revalidate: 900,  // 服务器重新生成内容的频率(秒)
    expire: 86400,    // 数据完全过期的时间(秒)
  })
  const data = await fetch('/api/data')
  return data
}

配置参数说明:

  • stale:客户端认为缓存数据新鲜的时间,在此期间不会向服务器发送请求
  • revalidate:服务器重新验证和重新生成缓存内容的频率
  • expire:缓存条目完全过期并从缓存中删除的时间

条件缓存生命周期

根据数据状态使用不同的缓存策略:

import { cacheLife, cacheTag } from 'next/cache'

async function getPostContent(slug: string) {
  'use cache'

  const post = await fetchPost(slug)
  cacheTag(`post-${slug}`)

  if (!post) {
    // 内容可能尚未发布或处于草稿状态
    // 短时间缓存以减少数据库负载
    cacheLife('minutes')
    return null
  }

  // 已发布的内容可以缓存更长时间
  cacheLife('days')
  return post.data
}

从数据动态计算缓存生命周期

import { cacheLife, cacheTag } from 'next/cache'

async function getPostContent(slug: string) {
  'use cache'

  const post = await fetchPost(slug)
  cacheTag(`post-${slug}`)

  if (!post) {
    cacheLife('minutes')
    return null
  }

  // 直接使用 CMS 数据中的缓存时间
  cacheLife({
    revalidate: post.revalidateSeconds ?? 3600,
  })

  return post.data
}

cacheTag 函数

cacheTag 函数允许标记缓存数据以进行按需失效。通过将标签与缓存条目关联,可以选择性地清除或重新验证特定缓存条目,而不影响其他缓存数据。

基本用法

import { cacheTag } from 'next/cache'

export async function getData() {
  'use cache'
  cacheTag('my-data')
  const data = await fetch('/api/data')
  return data
}

使用多个标签

import { cacheTag } from 'next/cache'

interface BookingsProps {
  type: string
}

export async function Bookings({ type = 'haircut' }: BookingsProps) {
  async function getBookingsData() {
    'use cache'
    const data = await fetch(`/api/bookings?type=${encodeURIComponent(type)}`)
    // 使用多个标签以实现灵活的失效策略
    cacheTag('bookings-data', `bookings-${type}`)
    return data
  }
  
  const bookings = await getBookingsData()
  return <div>{/* 渲染预订数据 */}</div>
}

失效标记的缓存

使用 revalidateTag 在需要时使特定标签的缓存失效:

'use server'

import { revalidateTag } from 'next/cache'

export async function updateBookings() {
  await updateBookingData()
  revalidateTag('bookings-data', 'max')
}

revalidateTag 函数

revalidateTag 允许按需使特定缓存标签的缓存数据失效。此函数适用于内容更新延迟可接受的场景,例如博客文章、产品目录或文档。

重新验证行为

重新验证行为取决于是否提供第二个参数:

  • 使用 profile="max"(推荐):标签条目被标记为陈旧,下次访问具有该标签的资源时,将使用 stale-while-revalidate 语义。这意味着在后台获取新鲜内容时提供陈旧内容。
  • 使用自定义缓存生命周期配置文件:可以指定应用程序定义的任何缓存生命周期配置文件,允许针对特定缓存需求的自定义重新验证行为。
  • 不使用第二个参数(已弃用):标签条目立即过期,对该资源的下一个请求将是阻塞重新验证/缓存未命中。

Server Action 中使用

'use server'

import { revalidateTag } from 'next/cache'

export default async function submit() {
  await addPost()
  revalidateTag('posts', 'max')
}

Route Handler 中使用

import type { NextRequest } from 'next/server'
import { revalidateTag } from 'next/cache'

export async function GET(request: NextRequest) {
  const tag = request.nextUrl.searchParams.get('tag')

  if (tag) {
    revalidateTag(tag, 'max')
    return Response.json({ revalidated: true, now: Date.now() })
  }

  return Response.json({
    revalidated: false,
    now: Date.now(),
    message: 'Missing tag to revalidate',
  })
}

Webhook 立即过期

对于需要立即过期的 webhook 或第三方服务,可以传递 { expire: 0 }

revalidateTag(tag, { expire: 0 })

updateTag 函数

updateTag 允许从 Server Actions 中按需更新特定缓存标签的缓存数据。此函数专为"读取自己的写入"场景设计,即用户进行更改(如创建帖子),UI 立即显示更改,而不是陈旧数据。

与 revalidateTag 的区别

  • updateTag:立即使指定标签的缓存数据过期。下一个请求将等待获取新鲜数据,而不是从缓存中提供陈旧内容,确保用户立即看到他们的更改。
  • revalidateTag:将标签条目标记为陈旧,在后台获取新鲜内容时提供陈旧内容。

基本用法

'use server'

import { updateTag } from 'next/cache'
import { redirect } from 'next/navigation'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  const content = formData.get('content') as string

  // 在数据库中创建帖子
  const post = await db.post.create({
    data: { title, content },
  })

  // 使缓存标签失效,以便新帖子立即可见
  updateTag('posts')
  updateTag(`post-${post.id}`)

  // 重定向到新帖子 - 用户将看到新鲜数据,而不是缓存数据
  redirect(`/posts/${post.id}`)
}

使用限制

updateTag 只能在 Server Actions 中调用,不能在 Route Handlers、Client Components 或其他上下文中使用。

import { updateTag } from 'next/cache'

export async function POST() {
  // 这将抛出错误
  updateTag('posts')
  // Error: updateTag can only be called from within a Server Action

  // 在 Route Handlers 中使用 revalidateTag
  revalidateTag('posts', 'max')
}

何时使用 updateTag

使用 updateTag 的场景:

  • 在 Server Action 中
  • 需要立即缓存失效以实现读取自己的写入
  • 希望确保下一个请求看到更新的数据

使用 revalidateTag 的场景:

  • 在 Route Handler 或其他非 action 上下文中
  • 希望使用 stale-while-revalidate 语义
  • 构建用于缓存失效的 webhook 或 API 端点

use cache: private

use cache: private 指令的工作方式与 use cache 类似,但允许使用运行时 API,如 cookiesheaderssearchParams

use cache 不同,私有缓存不会被静态预渲染,因为它们包含不在用户之间共享的个性化数据。

基本示例

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

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params

  return (
    <div>
      <ProductDetails id={id} />
      <Suspense fallback={<div>Loading recommendations...</div>}>
        <Recommendations productId={id} />
      </Suspense>
    </div>
  )
}

async function Recommendations({ productId }: { productId: string }) {
  const recommendations = await getRecommendations(productId)

  return (
    <div>
      {recommendations.map((rec) => (
        <ProductCard key={rec.id} product={rec} />
      ))}
    </div>
  )
}

async function getRecommendations(productId: string) {
  'use cache: private'
  cacheLife('minutes')

  const cookieStore = await cookies()
  const userId = cookieStore.get('userId')?.value

  const recommendations = await fetchRecommendations(productId, userId)
  cacheTag(`recommendations-${productId}-${userId}`)

  return recommendations
}

API 使用限制

APIuse cache 中允许use cache: private 中允许
cookies()
headers()
searchParams
connection()

use cache: remote

use cache: remote 指令在常规 use cache 无法工作的动态上下文中启用共享数据的缓存,例如在调用 await connection()await cookies()await headers() 之后。

结果存储在服务器端缓存处理程序中,并在所有用户之间共享。对于依赖于 await cookies()await headers() 的用户特定数据,请改用 use cache: private

基本示例

缓存需要在请求时获取但可以在所有用户之间共享的产品定价:

import { Suspense } from 'react'
import { connection } from 'next/server'
import { cacheLife, cacheTag } from 'next/cache'

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  await connection()
  const { id } = await params

  return (
    <div>
      <ProductDetails id={id} />
      <Suspense fallback={<div>Loading price...</div>}>
        <ProductPrice productId={id} />
      </Suspense>
    </div>
  )
}

async function ProductPrice({ productId }: { productId: string }) {
  const price = await getProductPrice(productId)
  return <div>Price: ${price}</div>
}

async function getProductPrice(productId: string) {
  'use cache: remote'
  cacheLife('minutes')

  const price = await fetchProductPrice(productId)
  cacheTag(`price-${productId}`)

  return price
}

何时使用

  • use cache:用于可以在构建时或首次请求时计算的共享数据
  • use cache: private:用于依赖于 cookies 或 headers 的用户特定数据
  • use cache: remote:用于在动态上下文中(调用 connection/cookies/headers 之后)需要缓存的共享数据

迁移指南

revalidate 迁移

使用 cacheLife 替代路由段配置:

// 之前
export const revalidate = 3600 // 1 小时

export default async function Page() {
  return <div>...</div>
}
// 之后 - 使用 cacheLife
import { cacheLife } from 'next/cache'

export default async function Page() {
  'use cache'
  cacheLife('hours')
  return <div>...</div>
}

fetchCache 迁移

不再需要。使用 use cache,缓存作用域内的所有数据获取都会自动缓存:

// 之前
export const fetchCache = 'force-cache'
// 之后 - 使用 'use cache' 控制缓存行为
export default async function Page() {
  'use cache'
  // 这里的所有 fetch 都会被缓存
  return <div>...</div>
}

Edge Runtime 不支持

Cache Components 需要 Node.js 运行时,在 Edge Runtime 中会抛出错误。

最佳实践

  1. 优先使用预设配置文件:使用 cacheLife 的预设配置文件(secondsminuteshoursdaysweeksmax)而不是自定义配置,除非有特定需求。

  2. 合理使用 Suspense:将动态内容包装在 <Suspense> 中,以改善用户体验并允许静态内容立即显示。

  3. 选择正确的缓存指令

    • 共享、静态数据使用 use cache
    • 用户特定数据使用 use cache: private
    • 动态上下文中的共享数据使用 use cache: remote
  4. 使用缓存标签进行精细控制:通过 cacheTag 标记缓存条目,以便在需要时进行选择性失效。

  5. 读取自己的写入使用 updateTag:在 Server Actions 中使用 updateTag 确保用户立即看到他们的更改。

  6. 后台更新使用 revalidateTag:对于可以接受短暂陈旧内容的场景,使用 revalidateTagprofile="max" 以实现更好的性能。

平台支持

部署选项支持
Node.js 服务器
Docker 容器
静态导出
适配器取决于平台

总结

Cache Components 是 Next.js 16 中的一项强大特性,通过允许在单个路由中混合静态、缓存和动态内容,提供了前所未有的灵活性和性能。通过合理使用 use cache 指令、cacheLife 函数、cacheTag 标记和失效策略,可以构建既快速又灵活的现代 Web 应用。

在 GitHub 上编辑

上次更新于