跳到内容
App Router入门缓存组件

缓存组件

缓存组件是 Next.js 中一种新的渲染和缓存方法,它通过**部分预渲染 (PPR)** 提供对缓存内容和时间的细粒度控制,同时确保出色的用户体验。

缓存组件

在开发动态应用程序时,您必须平衡两种主要方法

  • **完全静态页面**加载速度快,但无法显示个性化或实时数据
  • **完全动态页面**可以显示最新数据,但需要为每个请求渲染所有内容,导致初始加载速度变慢

启用缓存组件后,Next.js 默认将所有路由视为**动态路由**。每个请求都使用最新可用数据进行渲染。然而,大多数页面由静态和动态部分组成,并非所有动态数据都需要在每个请求中从源解析。

缓存组件允许您将数据甚至部分 UI 标记为可缓存,这些内容将与页面的静态部分一起包含在预渲染过程中。

**在缓存组件之前**,Next.js 尝试自动静态优化**整个**页面,这在添加动态代码时可能会导致意外行为。

缓存组件实现了**部分预渲染 (PPR)** 和 `use cache`,为您提供了两全其美的解决方案

Partially re-rendered Product Page showing static nav and product information, and dynamic cart and recommended products

当用户访问路由时

  • 服务器发送一个**静态 shell**,其中包含缓存内容,确保快速初始加载
  • 包裹在 `Suspense` 边界中的动态部分在 shell 中显示回退 UI
  • 只有动态部分会渲染以替换其回退,并在准备好时并行流式传输
  • 您可以通过使用 `use cache` 缓存数据,将原本动态的数据包含在初始 shell 中

**🎥 观看:** PPR 的原理及工作方式 → YouTube(10 分钟)

工作原理

缓存组件为您提供了三个关键工具来控制渲染

1. 运行时数据的 Suspense

某些数据仅在实际用户发出请求的运行时可用。`cookies`、`headers` 和 `searchParams` 等 API 访问请求特定信息。将使用这些 API 的组件包装在 `Suspense` 边界中,以便页面的其余部分可以作为静态 shell 进行预渲染。

运行时 API 包括

2. 动态数据的 Suspense

动态数据,如 `fetch` 调用或数据库查询 (`db.query(...)`),可能会在请求之间发生变化,但与用户无关。`connection` API 是元动态的——它表示等待用户导航,即使没有实际数据可返回。将使用这些内容的组件包装在 `Suspense` 边界中以启用流式传输。

动态数据模式包括

3. 使用 `use cache` 缓存数据

将 `use cache` 添加到任何服务器组件,使其被缓存并包含在预渲染的 shell 中。您不能在缓存组件中使用运行时 API。您还可以将实用函数标记为 `use cache` 并从服务器组件中调用它们。

export async function getProducts() {
  'use cache'
  const data = await db.query('SELECT * FROM products')
  return data
}

使用 Suspense 边界

React Suspense 边界允许您定义在包装动态或运行时数据时使用的回退 UI。

边界外部的内容(包括回退 UI)作为静态 shell 进行预渲染,而边界内部的内容在准备好后流式传输。

以下是如何将 `Suspense` 与缓存组件一起使用

app/page.tsx
import { Suspense } from 'react'
 
export default function Page() {
  return (
    <>
      <h1>This will be pre-rendered</h1>
      <Suspense fallback={<Skeleton />}>
        <DynamicContent />
      </Suspense>
    </>
  )
}
 
async function DynamicContent() {
  const res = await fetch('http://api.cms.com/posts')
  const { posts } = await res.json()
  return <div>{/* ... */}</div>
}

在构建时,Next.js 预渲染静态内容和 `fallback` UI,而动态内容则推迟到用户请求路由时。

**须知**:将组件包装在 `Suspense` 中并不会使其变为动态;您的 API 使用才是。`Suspense` 充当封装动态内容并启用流式传输的边界。

缺少 Suspense 边界

缓存组件强制动态代码必须包装在 `Suspense` 边界中。如果您忘记了,您将看到 `<Suspense>` 外部访问了未缓存的数据 错误

在 `<Suspense>` 外部访问了未缓存的数据

这会延迟整个页面的渲染,导致用户体验缓慢。Next.js 使用此错误来确保您的应用程序在每次导航时都能即时加载。

要解决此问题,您可以

**将组件包装在 `<Suspense>`** 边界中。这允许 Next.js 在内容准备好后立即将其流式传输给用户,而不会阻塞应用程序的其余部分。

或者

**将异步 await 移动到缓存组件("use cache")中**。这允许 Next.js 将组件作为 HTML 文档的一部分静态预渲染,因此用户可以即时看到它。

请注意,请求特定信息(如 params、cookies 和 headers)在静态预渲染期间不可用,因此必须将其包装在 `<Suspense>` 中。

此错误有助于防止出现用户无法立即获得静态 shell 的情况,取而代之的是遇到一个阻塞的运行时渲染,却没有任何内容可显示。要解决此问题,请添加 `Suspense` 边界或使用 `use cache` 来缓存工作。

流式传输的工作原理

流式传输将路由分成多个块,并在准备好时逐步将它们流式传输到客户端。这允许用户立即看到页面的部分内容,而无需等到整个内容渲染完成。

Diagram showing partially rendered page on the client, with loading UI for chunks that are being streamed.

通过部分预渲染,初始 UI 可以立即发送到浏览器,同时动态部分进行渲染。这减少了 UI 时间,并可能根据预渲染的 UI 量减少总请求时间。

Diagram showing parallelization of route segments during streaming, showing data fetching,rendering, and hydration of individual chunks.

为了减少网络开销,包括静态 HTML 和流式传输动态部分的完整响应会通过**单个 HTTP 请求**发送。这避免了额外的往返,并改善了初始加载和整体性能。

使用 `use cache`

虽然 `Suspense` 边界管理动态内容,但 `use cache` 指令可用于缓存不经常更改的数据或计算。

基本用法

添加 `use cache` 以缓存页面、组件或异步函数,并使用 `cacheLife` 定义生命周期

app/page.tsx
import { cacheLife } from 'next/cache'
 
export default async function Page() {
  'use cache'
  cacheLife('hours')
  // fetch or compute
  return <div>...</div>
}

注意事项

使用 `use cache` 时,请记住这些限制

参数必须可序列化

与服务器操作类似,缓存函数的参数必须可序列化。这意味着您可以传递原始类型、普通对象和数组,但不能传递类实例、函数或其他复杂类型。

在不自省的情况下接受不可序列化的值

您可以接受不可序列化的值作为参数,只要您不自省它们。但是,您可以返回它们。这允许像缓存组件接受服务器或客户端组件作为子组件的模式

app/cached-wrapper.tsx
import { ReactNode } from 'react'
 
export async function CachedWrapper({ children }: { children: ReactNode }) {
  'use cache'
  // Don't introspect children, just pass it through
  return (
    <div className="wrapper">
      <header>Cached Header</header>
      {children}
    </div>
  )
}

避免传递动态输入

您不得将动态或运行时数据传递给 `use cache` 函数,除非您避免自省它们。将 `cookies()`、`headers()` 或其他运行时 API 的值作为参数传递会导致错误,因为在预渲染时无法确定缓存键。

标签和重新验证

使用 `cacheTag` 标记缓存数据,并在变异后使用服务器操作中的 `updateTag` 进行立即更新,或者在可接受延迟更新的情况下使用 `revalidateTag` 重新验证。

使用 `updateTag`

当您需要立即在同一请求中使缓存数据过期并刷新时,请使用 `updateTag`

app/actions.ts
import { cacheTag, updateTag } from 'next/cache'
 
export async function getCart() {
  'use cache'
  cacheTag('cart')
  // fetch data
}
 
export async function updateCart(itemId: string) {
  'use server'
  // write data using the itemId
  // update the user cart
  updateTag('cart')
}

使用 `revalidateTag`

当您只想使正确标记的缓存条目失效,并采用陈旧数据重新验证策略时,请使用 `revalidateTag`。这非常适合可以容忍最终一致性的静态内容。

app/actions.ts
import { cacheTag, revalidateTag } from 'next/cache'
 
export async function getPosts() {
  'use cache'
  cacheTag('posts')
  // fetch data
}
 
export async function createPost(post: FormData) {
  'use server'
  // write data using the FormData
  revalidateTag('posts', 'max')
}

有关更详细的解释和用法示例,请参阅 `use cache` API 参考

启用缓存组件

您可以通过在 Next 配置文件中添加 `cacheComponents` 选项来启用缓存组件(包括 PPR)

next.config.ts
import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  cacheComponents: true,
}
 
export default nextConfig

对路由分段配置的影响

启用缓存组件后,一些路由分段配置选项不再需要或受支持。以下是更改内容以及如何迁移

`dynamic = "force-dynamic"`

**不需要。** 启用缓存组件后,所有页面默认都是动态的,因此此配置是不必要的。

// Before - No longer needed
export const dynamic = 'force-dynamic'
 
export default function Page() {
  return <div>...</div>
}
// After - Just remove it, pages are dynamic by default
export default function Page() {
  return <div>...</div>
}

`dynamic = "force-static"`

**替换为 `use cache`。** 您必须将 `use cache` 添加到相关路由的每个布局和页面中。

注意:`force-static` 以前允许使用 `cookies()` 等运行时 API,但现在不再支持。如果您添加 `use cache` 并看到与运行时数据相关的错误,则必须删除运行时 API 的使用。

// Before
export const dynamic = 'force-static'
 
export default async function Page() {
  const data = await fetch('https://api.example.com/data')
  return <div>...</div>
}
// After - Use 'use cache' instead
export default async function Page() {
  'use cache'
  const data = await fetch('https://api.example.com/data')
  return <div>...</div>
}

`revalidate`

**替换为 `cacheLife`。** 使用 `cacheLife` 函数来定义缓存持续时间,而不是路由分段配置。

// Before
export const revalidate = 3600 // 1 hour
 
export default async function Page() {
  return <div>...</div>
}
// After - Use cacheLife
import { cacheLife } from 'next/cache'
 
export default async function Page() {
  'use cache'
  cacheLife('hours')
  return <div>...</div>
}

`fetchCache`

**不需要。** 使用 `use cache`,缓存范围内的所有数据获取都会自动缓存,使 `fetchCache` 不再需要。

// Before
export const fetchCache = 'force-cache'
// After - Use 'use cache' to control caching behavior
export default async function Page() {
  'use cache'
  // All fetches here are cached
  return <div>...</div>
}

缓存组件之前与之后

了解缓存组件如何改变您的思维模式

缓存组件之前

  • **默认静态**:Next.js 尝试为您预渲染和缓存尽可能多的内容,除非您选择退出
  • **路由级别控制**:`dynamic`、`revalidate`、`fetchCache` 等开关控制整个页面的缓存
  • **`fetch` 的限制**:单独使用 `fetch` 是不完整的,因为它不涵盖直接数据库客户端或其他服务器端 IO。嵌套的 `fetch` 切换到动态(例如 ` { cache: 'no-store' }`)可能会无意中改变整个路由行为

使用缓存组件

  • **默认动态**:所有内容默认都是动态的。您可以通过在需要的地方添加 `use cache` 来决定缓存哪些部分
  • **细粒度控制**:文件/组件/函数级别的 `use cache``cacheLife` 精确控制您需要的缓存
  • **流式传输保持不变**:使用 `<Suspense>` 或 `loading.(js|tsx)` 文件在 shell 立即显示的同时流式传输动态部分
  • **超越 `fetch`**:使用 `use cache` 指令,缓存可以应用于所有服务器 IO(数据库调用、API、计算),而不仅仅是 `fetch`。嵌套的 `fetch` 调用不会悄悄地翻转整个路由,因为行为由显式缓存边界和 `Suspense` 控制

示例

动态 API

当访问 `cookies()` 等运行时 API 时,Next.js 只会预渲染此组件上方的回退 UI。

在此示例中,我们没有定义回退,因此 Next.js 显示一个错误,指示我们提供一个。`<User />` 组件需要包装在 `Suspense` 中,因为它使用了 `cookies` API

app/user.tsx
import { cookies } from 'next/headers'
 
export async function User() {
  const session = (await cookies()).get('session')?.value
  return '...'
}

现在我们的用户组件周围有一个 `Suspense` 边界,我们可以使用骨架 UI 预渲染页面,并在特定用户发出请求时流式传输 `<User />` UI

app/page.tsx
import { Suspense } from 'react'
import { User, AvatarSkeleton } from './user'
 
export default function Page() {
  return (
    <section>
      <h1>This will be pre-rendered</h1>
      <Suspense fallback={<AvatarSkeleton />}>
        <User />
      </Suspense>
    </section>
  )
}

传递动态 props

组件仅在访问值时才选择动态渲染。例如,如果您从 `<Page />` 组件读取 `searchParams`,您可以将此值作为 prop 转发给另一个组件

app/page.tsx
import { Table, TableSkeleton } from './table'
import { Suspense } from 'react'
 
export default function Page({
  searchParams,
}: {
  searchParams: Promise<{ sort: string }>
}) {
  return (
    <section>
      <h1>This will be pre-rendered</h1>
      <Suspense fallback={<TableSkeleton />}>
        <Table searchParams={searchParams.then((search) => search.sort)} />
      </Suspense>
    </section>
  )
}

在表格组件内部,访问 `searchParams` 中的值将使组件动态化,而页面的其余部分将被预渲染。

app/table.tsx
export async function Table({ sortPromise }: { sortPromise: Promise<string> }) {
  const sort = (await sortPromise) === 'true'
  return '...'
}

常见问题

这会取代部分预渲染 (PPR) 吗?

不。缓存组件**实现了** PPR 作为一项功能。旧的实验性 PPR 标志已被移除,但 PPR 将继续存在。

PPR 提供了静态 shell 和流式传输基础设施;`use cache` 允许您在有益时将优化的动态输出包含在该 shell 中。

我应该首先缓存什么?

您缓存的内容应该取决于您希望 UI 加载状态如何。如果数据不依赖于运行时数据,并且您接受在一段时间内为多个请求提供缓存值,请使用带有 `cacheLife` 的 `use cache` 来描述该行为。

对于具有更新机制的内容管理系统,请考虑使用具有更长缓存持续时间的标签,并依赖 `revalidateTag` 将静态初始 UI 标记为已准备好重新验证。此模式允许您提供快速的缓存响应,同时在内容实际更改时更新内容,而不是抢先使缓存过期。

如何快速更新缓存内容?

使用 `cacheTag` 标记您的缓存数据,然后触发 `updateTag``revalidateTag`