缓存组件
缓存组件是 Next.js 中一种新的渲染和缓存方法,它通过**部分预渲染 (PPR)** 提供对缓存内容和时间的细粒度控制,同时确保出色的用户体验。
缓存组件
在开发动态应用程序时,您必须平衡两种主要方法
- **完全静态页面**加载速度快,但无法显示个性化或实时数据
- **完全动态页面**可以显示最新数据,但需要为每个请求渲染所有内容,导致初始加载速度变慢
启用缓存组件后,Next.js 默认将所有路由视为**动态路由**。每个请求都使用最新可用数据进行渲染。然而,大多数页面由静态和动态部分组成,并非所有动态数据都需要在每个请求中从源解析。
缓存组件允许您将数据甚至部分 UI 标记为可缓存,这些内容将与页面的静态部分一起包含在预渲染过程中。
**在缓存组件之前**,Next.js 尝试自动静态优化**整个**页面,这在添加动态代码时可能会导致意外行为。
缓存组件实现了**部分预渲染 (PPR)** 和 `use cache`,为您提供了两全其美的解决方案

当用户访问路由时
- 服务器发送一个**静态 shell**,其中包含缓存内容,确保快速初始加载
- 包裹在 `Suspense` 边界中的动态部分在 shell 中显示回退 UI
- 只有动态部分会渲染以替换其回退,并在准备好时并行流式传输
- 您可以通过使用 `use cache` 缓存数据,将原本动态的数据包含在初始 shell 中
**🎥 观看:** PPR 的原理及工作方式 → YouTube(10 分钟)。
工作原理
缓存组件为您提供了三个关键工具来控制渲染
1. 运行时数据的 Suspense
某些数据仅在实际用户发出请求的运行时可用。`cookies`、`headers` 和 `searchParams` 等 API 访问请求特定信息。将使用这些 API 的组件包装在 `Suspense` 边界中,以便页面的其余部分可以作为静态 shell 进行预渲染。
运行时 API 包括
cookies
headers
- `searchParams` prop
- `params` prop - 除非您通过 `generateStaticParams` 提供至少一个示例值,否则这是运行时数据。提供后,这些特定的 param 值在预渲染路径中被视为静态,而其他值则保持运行时
2. 动态数据的 Suspense
动态数据,如 `fetch` 调用或数据库查询 (`db.query(...)`),可能会在请求之间发生变化,但与用户无关。`connection` API 是元动态的——它表示等待用户导航,即使没有实际数据可返回。将使用这些内容的组件包装在 `Suspense` 边界中以启用流式传输。
动态数据模式包括
- `fetch` 请求
- 数据库查询
connection
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` 与缓存组件一起使用
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` 来缓存工作。
流式传输的工作原理
流式传输将路由分成多个块,并在准备好时逐步将它们流式传输到客户端。这允许用户立即看到页面的部分内容,而无需等到整个内容渲染完成。

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

为了减少网络开销,包括静态 HTML 和流式传输动态部分的完整响应会通过**单个 HTTP 请求**发送。这避免了额外的往返,并改善了初始加载和整体性能。
使用 `use cache`
虽然 `Suspense` 边界管理动态内容,但 `use cache` 指令可用于缓存不经常更改的数据或计算。
基本用法
添加 `use cache` 以缓存页面、组件或异步函数,并使用 `cacheLife` 定义生命周期
import { cacheLife } from 'next/cache'
export default async function Page() {
'use cache'
cacheLife('hours')
// fetch or compute
return <div>...</div>
}
注意事项
使用 `use cache` 时,请记住这些限制
参数必须可序列化
与服务器操作类似,缓存函数的参数必须可序列化。这意味着您可以传递原始类型、普通对象和数组,但不能传递类实例、函数或其他复杂类型。
在不自省的情况下接受不可序列化的值
您可以接受不可序列化的值作为参数,只要您不自省它们。但是,您可以返回它们。这允许像缓存组件接受服务器或客户端组件作为子组件的模式
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`
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`。这非常适合可以容忍最终一致性的静态内容。
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)
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
import { cookies } from 'next/headers'
export async function User() {
const session = (await cookies()).get('session')?.value
return '...'
}
现在我们的用户组件周围有一个 `Suspense` 边界,我们可以使用骨架 UI 预渲染页面,并在特定用户发出请求时流式传输 `<User />` UI
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 转发给另一个组件
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` 中的值将使组件动态化,而页面的其余部分将被预渲染。
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`。
这有帮助吗?