缓存组件
深入理解 Next.js 缓存组件的特性,包括 use cache 指令、缓存生命周期管理、缓存标签和失效策略
Cache Components 是 Next.js 16 引入的一项重要特性,允许在单个路由中混合静态、缓存和动态内容,兼具静态站点的速度和动态渲染的灵活性。
核心概念
传统的服务端渲染应用通常需要在静态页面(快速但内容陈旧)和动态页面(内容新鲜但速度慢)之间做出选择。将工作转移到客户端虽然减轻了服务器负载,但会增加打包体积并降低初始渲染速度。
Cache Components 通过将路由预渲染为静态 HTML 外壳来消除这些权衡,该外壳立即发送到浏览器,动态内容在准备就绪时更新 UI。
渲染工作流程
在构建时,Next.js 渲染路由的组件树。只要组件不访问网络资源、某些系统 API 或不需要传入请求来渲染,它们的输出就会自动添加到静态外壳中。否则,你必须选择如何处理它们:
- 延迟渲染:使用 React 的
<Suspense>包装组件,在内容准备就绪之前显示后备 UI - 缓存结果:使用
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 = nextConfiguse 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()searchParamsconnection()
如果需要使用这些 API,请使用 use cache: private 或 use 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,如 cookies、headers 或 searchParams。
与 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 使用限制
| API | use 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 中会抛出错误。
最佳实践
-
优先使用预设配置文件:使用
cacheLife的预设配置文件(seconds、minutes、hours、days、weeks、max)而不是自定义配置,除非有特定需求。 -
合理使用 Suspense:将动态内容包装在
<Suspense>中,以改善用户体验并允许静态内容立即显示。 -
选择正确的缓存指令:
- 共享、静态数据使用
use cache - 用户特定数据使用
use cache: private - 动态上下文中的共享数据使用
use cache: remote
- 共享、静态数据使用
-
使用缓存标签进行精细控制:通过
cacheTag标记缓存条目,以便在需要时进行选择性失效。 -
读取自己的写入使用 updateTag:在 Server Actions 中使用
updateTag确保用户立即看到他们的更改。 -
后台更新使用 revalidateTag:对于可以接受短暂陈旧内容的场景,使用
revalidateTag与profile="max"以实现更好的性能。
平台支持
| 部署选项 | 支持 |
|---|---|
| Node.js 服务器 | 是 |
| Docker 容器 | 是 |
| 静态导出 | 否 |
| 适配器 | 取决于平台 |
总结
Cache Components 是 Next.js 16 中的一项强大特性,通过允许在单个路由中混合静态、缓存和动态内容,提供了前所未有的灵活性和性能。通过合理使用 use cache 指令、cacheLife 函数、cacheTag 标记和失效策略,可以构建既快速又灵活的现代 Web 应用。
上次更新于