Next.js

缓存与重新验证

深入理解 Next.js 缓存机制和重新验证策略,包括请求记忆化、数据缓存、完整路由缓存、路由器缓存及相关 API

概述

缓存是一种存储数据获取和其他计算结果的技术,以便未来对相同数据的请求可以更快地提供服务,而无需再次执行工作。重新验证允许你更新缓存条目,而无需重建整个应用程序。

Next.js 提供了几个 API 来处理缓存和重新验证:

  • fetch
  • cacheTag
  • revalidateTag
  • updateTag
  • revalidatePath
  • unstable_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 方法,其他方法(如 POSTDELETE)不会被记忆化。此默认行为是 React 优化,我们不建议选择退出。

要管理单个请求,可以使用 AbortControllersignal 属性:

const { signal } = new AbortController()
fetch(url, { signal })

数据缓存

Next.js 有一个内置的数据缓存,可以在传入的服务器请求和部署之间持久化数据获取的结果。这是可能的,因为 Next.js 扩展了原生 fetch API,允许服务器上的每个请求设置自己的持久化缓存语义。

工作原理

  • 第一次在渲染期间调用 fetch 请求时,Next.js 检查数据缓存中是否有缓存的响应
  • 如果找到缓存的响应,它会立即返回并被记忆化
  • 如果未找到缓存的响应,则向数据源发出请求,结果存储在数据缓存中并被记忆化
  • 对于未缓存的数据(例如 { cache: 'no-store' }),结果始终从数据源获取并被记忆化
  • 无论数据是缓存还是未缓存,请求始终被记忆化以避免在 React 渲染过程中对相同数据发出重复请求

持续时间

数据缓存在传入请求和部署之间是持久的,除非你重新验证或选择退出。

重新验证

可以通过两种方式重新验证缓存数据:

  1. 基于时间的重新验证:在经过一定时间并发出新请求后重新验证数据。这对于不经常更改且新鲜度不那么重要的数据很有用。

  2. 按需重新验证:基于事件(例如表单提交)重新验证数据。按需重新验证可以使用基于标签或基于路径的方法一次性重新验证数据组。

完整路由缓存

Next.js 在构建时自动渲染和缓存路由。这是一种优化,允许你提供缓存的路由而不是在每次请求时在服务器上渲染,从而加快页面加载速度。

工作原理

  • 在构建期间,Next.js 渲染路由并将结果(HTML 和 RSC 负载)存储在完整路由缓存中
  • 当用户访问路由时,Next.js 提供缓存的 HTML 和 RSC 负载
  • 对于动态路由,Next.js 在第一次访问时渲染路由并缓存结果

持续时间

默认情况下,完整路由缓存是持久的。这意味着渲染输出跨用户请求缓存。

失效

可以通过两种方式使完整路由缓存失效:

  1. 重新验证数据:重新验证数据缓存将通过在服务器上重新渲染组件并缓存新的渲染输出来使路由器缓存失效
  2. 重新部署:与跨部署持久化的数据缓存不同,完整路由缓存在新部署时被清除

路由器缓存

Next.js 有一个内存中的客户端缓存,用于存储 RSC 负载,按单个路由段拆分,用于用户会话的持续时间。这称为路由器缓存。

工作原理

  • 当用户在路由之间导航时,Next.js 缓存访问过的路由段并预取用户可能导航到的路由
  • 这导致即时的后退/前进导航,导航之间没有完整页面重新加载,并保留 React 状态和浏览器状态

持续时间

缓存存储在浏览器的临时内存中。两个因素决定路由器缓存持续多长时间:

  • 会话:缓存在导航之间持续存在。但是,它在页面刷新时被清除
  • 自动失效期:单个段的缓存在特定时间后自动失效
    • 动态渲染:30 秒
    • 静态渲染:5 分钟

失效

有两种方式可以使路由器缓存失效:

  1. 在 Server Action 中
    • 使用 revalidatePath 按路径按需重新验证数据
    • 使用 revalidateTag 按缓存标签按需重新验证数据
  2. 使用 cookies.setcookies.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:要缓存的异步函数,必须返回 Promise
  • keyParts:额外的键数组,用于进一步标识缓存。默认情况下,unstable_cache 已经使用参数和函数的字符串化版本作为缓存键
  • options:控制缓存行为的对象
    • tags:用于控制缓存失效的标签数组
    • revalidate:缓存应重新验证的秒数。省略或传递 false 以无限期缓存,直到调用匹配的 revalidateTag()revalidatePath() 方法

注意事项

  • 不支持在缓存作用域内访问动态数据源,如 headerscookies。如果需要在缓存函数内使用这些数据,请在缓存函数外使用 headers,并将所需的动态数据作为参数传入
  • 此 API 使用 Next.js 的内置数据缓存在请求和部署之间持久化结果
  • 此 API 将被 use cache 替代

React cache 函数

React cache 函数允许你记忆函数的返回值,允许你多次调用同一个函数但只执行一次。

fetch 请求使用 GETHEAD 方法会自动记忆化,因此你不需要将其包装在 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()
  • searchParams prop
  • draftMode()

使用这些函数中的任何一个都会选择整个路由在请求时进行动态渲染。

缓存配置选项

路由段配置

可以使用路由段配置选项来配置缓存行为:

// 选择退出所有数据缓存
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. 使用缓存标签进行精细控制

使用 cacheTagrevalidateTag 进行精细的缓存失效控制:

// 标记数据
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 提供了强大而灵活的缓存系统:

  1. 四种缓存机制:请求记忆化、数据缓存、完整路由缓存、路由器缓存
  2. 多个 API:fetch、cacheTag、revalidateTag、updateTag、revalidatePath、unstable_cache
  3. 灵活的策略:基于时间的重新验证、按需重新验证、混合策略
  4. 自动优化:默认缓存尽可能多的内容以提高性能
  5. 精细控制:使用标签和路径进行精确的缓存失效

通过合理使用这些缓存机制和 API,可以构建既快速又灵活的现代 Web 应用。

在 GitHub 上编辑

上次更新于