缓存与重新验证
深入理解 Next.js 缓存机制和重新验证策略,包括请求记忆化、数据缓存、完整路由缓存、路由器缓存及相关 API
概述
缓存是一种存储数据获取和其他计算结果的技术,以便未来对相同数据的请求可以更快地提供服务,而无需再次执行工作。重新验证允许你更新缓存条目,而无需重建整个应用程序。
Next.js 提供了几个 API 来处理缓存和重新验证:
fetchcacheTagrevalidateTagupdateTagrevalidatePathunstable_cache(遗留)
缓存机制
Next.js 有四种不同的缓存机制:
| 机制 | 缓存内容 | 位置 | 目的 | 持续时间 |
|---|---|---|---|---|
| 请求记忆化 | 函数返回值 | 服务器 | 在 React 组件树中重用数据 | 每个请求生命周期 |
| 数据缓存 | 数据 | 服务器 | 跨用户请求和部署存储数据 | 持久化(可重新验证) |
| 完整路由缓存 | HTML 和 RSC 负载 | 服务器 | 降低渲染成本并提高性能 | 持久化(可重新验证) |
| 路由器缓存 | RSC 负载 | 客户端 | 减少导航时的服务器请求 | 用户会话或基于时间 |
默认情况下,Next.js 会尽可能多地缓存以提高性能并降低成本。
请求记忆化
React 扩展了 fetch API,自动记忆具有相同 URL 和选项的请求。这意味着你可以在组件树的多个位置调用相同的数据获取函数,但只执行一次。
async function getUser() {
const res = await fetch('https://api.example.com/user')
return res.json()
}
export default async function Page() {
// 这两个调用只会发出一次网络请求
const user1 = await getUser()
const user2 = await getUser()
return <div>{user1.name}</div>
}工作原理
- 在渲染路由时,第一次调用特定请求时,其结果不会在内存中,将是缓存未命中
- 因此,函数将被执行,数据将从外部源获取,结果将存储在内存中
- 同一渲染过程中对该请求的后续函数调用将是缓存命中,数据将从内存返回而无需执行函数
- 一旦路由被渲染并且渲染过程完成,内存将被重置,所有请求记忆化条目将被清除
持续时间
缓存持续服务器请求的生命周期,直到 React 组件树完成渲染。
重新验证
由于记忆化不跨服务器请求共享,仅在渲染期间应用,因此无需重新验证它。
选择退出
记忆化仅适用于 fetch 请求中的 GET 方法,其他方法(如 POST 和 DELETE)不会被记忆化。此默认行为是 React 优化,我们不建议选择退出。
要管理单个请求,可以使用 AbortController 的 signal 属性:
const { signal } = new AbortController()
fetch(url, { signal })数据缓存
Next.js 有一个内置的数据缓存,可以在传入的服务器请求和部署之间持久化数据获取的结果。这是可能的,因为 Next.js 扩展了原生 fetch API,允许服务器上的每个请求设置自己的持久化缓存语义。
工作原理
- 第一次在渲染期间调用
fetch请求时,Next.js 检查数据缓存中是否有缓存的响应 - 如果找到缓存的响应,它会立即返回并被记忆化
- 如果未找到缓存的响应,则向数据源发出请求,结果存储在数据缓存中并被记忆化
- 对于未缓存的数据(例如
{ cache: 'no-store' }),结果始终从数据源获取并被记忆化 - 无论数据是缓存还是未缓存,请求始终被记忆化以避免在 React 渲染过程中对相同数据发出重复请求
持续时间
数据缓存在传入请求和部署之间是持久的,除非你重新验证或选择退出。
重新验证
可以通过两种方式重新验证缓存数据:
-
基于时间的重新验证:在经过一定时间并发出新请求后重新验证数据。这对于不经常更改且新鲜度不那么重要的数据很有用。
-
按需重新验证:基于事件(例如表单提交)重新验证数据。按需重新验证可以使用基于标签或基于路径的方法一次性重新验证数据组。
完整路由缓存
Next.js 在构建时自动渲染和缓存路由。这是一种优化,允许你提供缓存的路由而不是在每次请求时在服务器上渲染,从而加快页面加载速度。
工作原理
- 在构建期间,Next.js 渲染路由并将结果(HTML 和 RSC 负载)存储在完整路由缓存中
- 当用户访问路由时,Next.js 提供缓存的 HTML 和 RSC 负载
- 对于动态路由,Next.js 在第一次访问时渲染路由并缓存结果
持续时间
默认情况下,完整路由缓存是持久的。这意味着渲染输出跨用户请求缓存。
失效
可以通过两种方式使完整路由缓存失效:
- 重新验证数据:重新验证数据缓存将通过在服务器上重新渲染组件并缓存新的渲染输出来使路由器缓存失效
- 重新部署:与跨部署持久化的数据缓存不同,完整路由缓存在新部署时被清除
路由器缓存
Next.js 有一个内存中的客户端缓存,用于存储 RSC 负载,按单个路由段拆分,用于用户会话的持续时间。这称为路由器缓存。
工作原理
- 当用户在路由之间导航时,Next.js 缓存访问过的路由段并预取用户可能导航到的路由
- 这导致即时的后退/前进导航,导航之间没有完整页面重新加载,并保留 React 状态和浏览器状态
持续时间
缓存存储在浏览器的临时内存中。两个因素决定路由器缓存持续多长时间:
- 会话:缓存在导航之间持续存在。但是,它在页面刷新时被清除
- 自动失效期:单个段的缓存在特定时间后自动失效
- 动态渲染:30 秒
- 静态渲染:5 分钟
失效
有两种方式可以使路由器缓存失效:
- 在 Server Action 中:
- 使用
revalidatePath按路径按需重新验证数据 - 使用
revalidateTag按缓存标签按需重新验证数据
- 使用
- 使用
cookies.set或cookies.delete:使路由器缓存失效以防止使用 cookie 的路由变得陈旧
fetch API
默认情况下,fetch 请求不会被缓存。你可以通过将 cache 选项设置为 'force-cache' 来缓存单个请求。
export default async function Page() {
const data = await fetch('https://...', { cache: 'force-cache' })
const posts = await data.json()
return <ul>{/* 渲染帖子 */}</ul>
}重新验证 fetch 数据
要重新验证 fetch 请求返回的数据,可以使用 next.revalidate 选项:
export default async function Page() {
// 每小时重新验证
const data = await fetch('https://...', { next: { revalidate: 3600 } })
const posts = await data.json()
return <ul>{/* 渲染帖子 */}</ul>
}使用缓存标签
可以使用 next.tags 选项为 fetch 请求添加缓存标签:
export default async function Page() {
const data = await fetch('https://...', {
next: { tags: ['posts'] }
})
const posts = await data.json()
return <ul>{/* 渲染帖子 */}</ul>
}然后可以使用 revalidateTag 按需重新验证:
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost() {
// 创建帖子
revalidateTag('posts', 'max')
}cacheTag
cacheTag 函数允许你标记缓存数据以进行按需失效(已在 Cache Components 笔记中详细介绍)。
import { cacheTag } from 'next/cache'
export async function getData() {
'use cache'
cacheTag('my-data')
const data = await fetch('/api/data')
return data
}revalidateTag
revalidateTag 允许你按需使特定缓存标签的缓存数据失效(已在 Cache Components 和 Updating Data 笔记中详细介绍)。
'use server'
import { revalidateTag } from 'next/cache'
export async function updateData() {
await updateDatabase()
revalidateTag('my-data', 'max')
}updateTag
updateTag 允许你从 Server Actions 中按需更新特定缓存标签的缓存数据(已在 Cache Components 和 Updating Data 笔记中详细介绍)。
'use server'
import { updateTag } from 'next/cache'
export async function createData() {
await createInDatabase()
updateTag('my-data')
}revalidatePath
revalidatePath 允许你按需使特定路径的缓存数据失效(已在 Updating Data 笔记中详细介绍)。
'use server'
import { revalidatePath } from 'next/cache'
export async function updatePost() {
await updatePostInDatabase()
revalidatePath('/blog/[slug]', 'page')
}unstable_cache(遗留)
unstable_cache 允许你缓存昂贵操作的结果,如数据库查询,并在多个请求之间重用它们。
import { unstable_cache } from 'next/cache'
import { getUser } from './data'
export default async function Page({
params,
}: {
params: Promise<{ userId: string }>
}) {
const { userId } = await params
const getCachedUser = unstable_cache(
async () => {
return getUser(userId)
},
[userId], // 将用户 ID 添加到缓存键
{
tags: ['users'],
revalidate: 3600,
}
)
const user = await getCachedUser()
return <div>{user.name}</div>
}参数
unstable_cache(fetchData, keyParts, options)fetchData:要缓存的异步函数,必须返回PromisekeyParts:额外的键数组,用于进一步标识缓存。默认情况下,unstable_cache已经使用参数和函数的字符串化版本作为缓存键options:控制缓存行为的对象tags:用于控制缓存失效的标签数组revalidate:缓存应重新验证的秒数。省略或传递false以无限期缓存,直到调用匹配的revalidateTag()或revalidatePath()方法
注意事项
- 不支持在缓存作用域内访问动态数据源,如
headers或cookies。如果需要在缓存函数内使用这些数据,请在缓存函数外使用headers,并将所需的动态数据作为参数传入 - 此 API 使用 Next.js 的内置数据缓存在请求和部署之间持久化结果
- 此 API 将被
use cache替代
React cache 函数
React cache 函数允许你记忆函数的返回值,允许你多次调用同一个函数但只执行一次。
fetch 请求使用 GET 或 HEAD 方法会自动记忆化,因此你不需要将其包装在 React cache 中。但是,对于其他 fetch 方法,或者当使用不固有记忆化请求的数据获取库(如某些数据库、CMS 或 GraphQL 客户端)时,你可以使用 cache 手动记忆化数据请求。
import { cache } from 'react'
import db from '@/lib/db'
export const getItem = cache(async (id: string) => {
const item = await db.item.findUnique({ id })
return item
})现在可以在多个组件中调用 getItem,但只会执行一次数据库查询:
import { getItem } from '@/utils/get-item'
export default async function Layout({
params,
children,
}: {
params: Promise<{ id: string }>
children: React.ReactNode
}) {
const { id } = await params
const item = await getItem(id)
return (
<>
<h1>{item.name}</h1>
{children}
</>
)
}import { getItem } from '@/utils/get-item'
export default async function Page({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
// 这不会发出第二次数据库查询
const item = await getItem(id)
return <div>{item.description}</div>
}静态渲染与动态渲染
静态渲染
在静态渲染中,路由在构建时或数据重新验证后在后台渲染。结果被缓存并可以推送到 CDN。此优化允许你在用户和服务器请求之间共享渲染工作的结果。
当路由具有在构建时已知的数据或不是个性化给用户的数据时,静态渲染很有用。
动态渲染
在动态渲染中,路由在请求时为每个用户渲染。
当路由具有个性化给用户的数据或只能在请求时知道的信息(如 cookies 或 URL 的搜索参数)时,动态渲染很有用。
切换到动态渲染
在渲染期间,如果发现动态函数或未缓存的数据请求,Next.js 将切换到动态渲染整个路由。
动态函数包括:
cookies()headers()connection()searchParamspropdraftMode()
使用这些函数中的任何一个都会选择整个路由在请求时进行动态渲染。
缓存配置选项
路由段配置
可以使用路由段配置选项来配置缓存行为:
// 选择退出所有数据缓存
export const dynamic = 'force-dynamic'
// 重新验证此页面每 60 秒
export const revalidate = 60
export default async function Page() {
const data = await fetch('https://...')
return <div>{/* 内容 */}</div>
}generateStaticParams
对于动态段(例如 app/blog/[slug]/page.tsx),可以使用 generateStaticParams 提供的路径在构建时静态生成:
export async function generateStaticParams() {
const posts = await fetch('https://...').then((res) => res.json())
return posts.map((post) => ({
slug: post.slug,
}))
}
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await fetch(`https://.../${slug}`)
return <article>{/* 内容 */}</article>
}缓存最佳实践
1. 使用适当的缓存策略
- 对于很少更改的数据,使用
cache: 'force-cache' - 对于频繁更改的数据,使用
next.revalidate - 对于实时数据,使用
cache: 'no-store'
2. 使用缓存标签进行精细控制
使用 cacheTag 和 revalidateTag 进行精细的缓存失效控制:
// 标记数据
const data = await fetch('https://...', {
next: { tags: ['posts', 'featured'] }
})
// 按需失效
revalidateTag('posts', 'max')3. 利用请求记忆化
在同一渲染过程中多次调用相同的函数时,利用自动请求记忆化:
async function getUser() {
return fetch('https://...')
}
export default async function Page() {
// 只发出一次请求
const user1 = await getUser()
const user2 = await getUser()
}4. 使用 React cache 进行非 fetch 请求
对于数据库查询或其他非 fetch 请求,使用 React cache:
import { cache } from 'react'
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } })
})5. 选择正确的重新验证策略
- 基于时间:对于定期更新的内容(新闻、博客)
- 按需:对于用户操作触发的更新(表单提交、删除)
6. 监控缓存性能
使用 Next.js 的内置分析和日志来监控缓存命中率和性能。
常见模式
增量静态再生成(ISR)
结合静态生成和重新验证:
export const revalidate = 3600 // 每小时重新验证
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await fetch(`https://.../${slug}`)
return <article>{/* 内容 */}</article>
}按需重新验证
在数据更新时立即重新验证:
'use server'
import { revalidatePath } from 'next/cache'
export async function updatePost(slug: string) {
await updatePostInDatabase(slug)
revalidatePath(`/blog/${slug}`)
}混合缓存策略
在同一页面中混合不同的缓存策略:
export default async function Page() {
// 缓存的静态数据
const staticData = await fetch('https://...', {
cache: 'force-cache'
})
// 定期重新验证的数据
const revalidatedData = await fetch('https://...', {
next: { revalidate: 60 }
})
// 实时数据
const liveData = await fetch('https://...', {
cache: 'no-store'
})
return <div>{/* 渲染所有数据 */}</div>
}总结
Next.js 提供了强大而灵活的缓存系统:
- 四种缓存机制:请求记忆化、数据缓存、完整路由缓存、路由器缓存
- 多个 API:fetch、cacheTag、revalidateTag、updateTag、revalidatePath、unstable_cache
- 灵活的策略:基于时间的重新验证、按需重新验证、混合策略
- 自动优化:默认缓存尽可能多的内容以提高性能
- 精细控制:使用标签和路径进行精确的缓存失效
通过合理使用这些缓存机制和 API,可以构建既快速又灵活的现代 Web 应用。
上次更新于