须知: 缓存组件(Cache Components)是一个可选功能。通过在 Next 配置文件中将 cacheComponents 标志设置为 true 来启用它。有关更多详细信息,请参阅启用缓存组件。
缓存组件(Cache Components)允许您在单个路由中混合静态、缓存和动态内容,从而在拥有动态渲染灵活性的同时,享受静态站点的速度。
服务器渲染的应用程序通常会强制您在静态页面(快速但陈旧)和动态页面(新鲜但缓慢)之间做出选择。将这项工作转移到客户端会以更大的打包文件和更慢的初始渲染为代价,减少服务器负载。
缓存组件(Cache Components)通过将路由预渲染为静态 HTML 外壳来消除这些权衡,该外壳会立即发送到浏览器,动态内容会在准备就绪时更新 UI。

在构建时,Next.js 会渲染您的路由组件树。只要组件不访问网络资源、特定的系统 API 或需要传入请求才能渲染,它们的输出就会自动添加到静态外壳中。否则,您必须选择如何处理它们:
<Suspense> 中,将渲染推迟到请求时,并在内容准备好之前显示回退 UI,或者use cache 指令缓存结果,以将其包含在静态外壳中(如果不需要请求数据)由于这在请求到达之前就已经发生,因此我们将其称为预渲染。这会生成一个静态外壳,其中包含用于初始页面加载的 HTML 和用于客户端导航的序列化 RSC Payload,确保无论用户是直接导航到 URL 还是从另一个页面过渡,浏览器都能立即收到完全渲染的内容。
Next.js 要求您明确处理在预渲染期间无法完成的组件。如果它们没有被 <Suspense> 包裹或没有标记 use cache,您将在开发和构建时看到 Uncached data was accessed outside of <Suspense> 错误。
须知:缓存可以在组件或函数级别应用,而回退 UI 可以在任何子树周围定义,这意味着您可以在单个路由中组合静态、缓存和动态内容。

这种渲染方法称为部分预渲染(Partial Prerendering),它是缓存组件(Cache Components)的默认行为。在本文档的其余部分,我们将其简称为“预渲染”,它可以生成部分或完整的输出。
🎥 观看: 什么是部分预渲染及其工作原理 → YouTube (10 分钟)。
同步 I/O、模块导入和纯计算等操作可以在预渲染期间完成。仅使用这些操作的组件的渲染输出将包含在静态 HTML 外壳中。
因为下面 Page 组件中的所有操作都在渲染期间完成,所以其渲染输出会自动包含在静态外壳中。当布局和页面都成功预渲染时,整个路由就是静态外壳。
import fs from "node:fs";
export default async function Page() {
// Synchronous file system read
const content = fs.readFileSync("./config.json", "utf-8");
// Module imports
const constants = await import("./constants.json");
// Pure computations
const processed = JSON.parse(content).items.map((item) => item.value * 2);
return (
<div>
<h1>{constants.appName}</h1>
<ul>
{processed.map((value, i) => (
<li key={i}>{value}</li>
))}
</ul>
</div>
);
}须知:您可以通过检查构建输出摘要来验证路由是否已完全预渲染。或者,通过在浏览器中查看页面源代码,查看添加到任何页面的静态外壳中的内容。
在预渲染期间,当 Next.js 遇到无法完成的工作(例如网络请求、访问请求数据或异步操作)时,它要求您明确处理。为了将渲染推迟到请求时,父组件必须使用 Suspense 边界提供回退 UI。回退成为静态外壳的一部分,而实际内容在请求时解析。
将 Suspense 边界尽可能靠近需要它们的组件。这最大限度地增加了静态外壳中的内容量,因为边界之外的所有内容仍然可以正常预渲染。
须知:通过 Suspense 边界,多个动态部分可以并行渲染,而不是互相阻塞,从而减少总加载时间。
外部系统异步提供内容,这通常需要不可预测的时间来解析,甚至可能失败。这就是预渲染不自动执行它们的原因。
通常,当您在每个请求中都需要来自源的最新数据(例如实时 Feed 或个性化内容)时,请通过提供带有 Suspense 边界的回退 UI 来推迟渲染。
例如,下面的 DynamicContent 组件使用了多个不会自动预渲染的操作。
import { Suspense } from 'react'
import fs from 'node:fs/promises'
import { setTimeout } = 'node:timers/promises'
async function DynamicContent() {
// Network request
const data = await fetch('https://api.example.com/data')
// Database query
const users = await db.query('SELECT * FROM users')
// Async file system operation
const file = await fs.readFile('..', 'utf-8')
// Simulating external system delay
await setTimeout(100) // from 'node:timers/promises'
return <div>Not in the static shell</div>
}要在页面中使用 DynamicContent,请将其包装在 <Suspense> 中以定义回退 UI:
export default async function Page(props) {
return (
<>
<h1>Part of the static shell</h1>
{/* <p>Loading..</p> is part of the static shell */}
<Suspense fallback={<p>Loading..</p>}>
<DynamicContent />
<div>Sibling excluded from static shell</div>
</Suspense>
</>
);
}预渲染在 fetch 请求处停止。请求本身不会启动,其后的任何代码都不会执行。
回退(<p>Loading...</p>)包含在静态外壳中,而组件的内容则在请求时流式传输。
在此示例中,由于所有操作(网络请求、数据库查询、文件读取和超时)都在同一组件中按顺序运行,因此内容直到它们全部完成后才会出现。
须知:对于不经常更改的动态内容,您可以使用 use cache 将动态数据包含在静态外壳中,而不是流式传输它。有关示例,请参阅预渲染期间部分。
一种需要请求上下文的特定类型的动态数据,仅在用户发出请求时可用。
cookies() - 用户的 cookie 数据headers() - 请求头searchParams - URL 查询参数params - 动态路由参数(除非通过 generateStaticParams 提供了至少一个示例)import { cookies, headers } from "next/headers";
import { Suspense } from "react";
async function RuntimeData({ searchParams }) {
// Accessing request data
const cookieStore = await cookies();
const headerStore = await headers();
const search = await searchParams;
return <div>Not in the static shell</div>;
}要使用 RuntimeData 组件,请将其包装在 <Suspense> 边界中:
export default async function Page(props) {
return (
<>
<h1>Part of the static shell</h1>
{/* <p>Loading..</p> is part of the static shell */}
<Suspense fallback={<p>Loading..</p>}>
<RuntimeData searchParams={props.searchParams} />
<div>Sibling excluded from static shell</div>
</Suspense>
</>
);
}如果您需要在不访问上述任何运行时 API 的情况下推迟到请求时,请使用 connection()。
须知:运行时数据不能使用 use cache 进行缓存,因为它需要请求上下文。访问运行时 API 的组件必须始终包装在 <Suspense> 中。但是,您可以从运行时数据中提取值并将它们作为参数传递给缓存函数。有关示例,请参阅使用运行时数据部分。
像 Math.random()、Date.now() 或 crypto.randomUUID() 这样的操作每次执行都会产生不同的值。为了确保这些操作在请求时运行(为每个请求生成唯一值),缓存组件(Cache Components)要求您通过在动态或运行时数据访问之后调用这些操作来明确表示此意图。
import { connection } from "next/server";
import { Suspense } from "react";
async function UniqueContent() {
// Explicitly defer to request time
await connection();
// Non-deterministic operations
const random = Math.random();
const now = Date.now();
const date = new Date();
const uuid = crypto.randomUUID();
const bytes = crypto.getRandomValues(new Uint8Array(16));
return (
<div>
<p>{random}</p>
<p>{now}</p>
<p>{date.getTime()}</p>
<p>{uuid}</p>
<p>{bytes}</p>
</div>
);
}因为 UniqueContent 组件推迟到请求时,所以要在路由中使用它,必须将其包装在 <Suspense> 中:
export default async function Page() {
return (
// <p>Loading..</p> is part of the static shell
<Suspense fallback={<p>Loading..</p>}>
<UniqueContent />
</Suspense>
);
}每个传入的请求都会看到不同的随机数、日期等。
须知:您可以使用 use cache 缓存非确定性操作。有关示例,请参阅使用非确定性操作部分。
use cacheuse cache 指令缓存异步函数和组件的返回值。您可以将其应用于函数、组件或文件级别。
参数和父作用域中任何闭包捕获的值都会自动成为缓存键的一部分,这意味着不同的输入会产生单独的缓存条目。这使得个性化或参数化缓存内容成为可能。
当动态内容不需要在每个请求中从源重新获取最新数据时,缓存它允许您在预渲染期间将内容包含在静态外壳中,或者在运行时跨多个请求重用结果。
缓存内容可以通过两种方式重新验证:根据缓存生命周期自动重新验证,或使用 revalidateTag 或 updateTag 按需使用标签进行重新验证。
须知:有关可以缓存的内容以及参数如何工作的详细信息,请参阅序列化要求和约束。
虽然动态内容是从外部源获取的,但它在访问之间不太可能发生变化。产品目录数据随库存变化而更新,博客文章内容在发布后很少更改,过去日期的分析报告保持静态。
如果此数据不依赖于运行时数据,则可以使用 use cache 指令将其包含在静态 HTML 外壳中。使用 cacheLife 定义缓存数据的使用时长。
当重新验证发生时,静态外壳会使用新内容进行更新。有关按需重新验证的详细信息,请参阅标记和重新验证。
import { cacheLife } from "next/cache";
export default async function Page() {
"use cache";
cacheLife("hours");
const users = await db.query("SELECT * FROM users");
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}cacheLife 函数接受缓存配置文件名称(如 'hours'、'days' 或 'weeks')或自定义配置对象来控制缓存行为:
import { cacheLife } from "next/cache";
export default async function Page() {
"use cache";
cacheLife({
stale: 3600, // 1 hour until considered stale
revalidate: 7200, // 2 hours until revalidated
expire: 86400, // 1 day until expired
});
const users = await db.query("SELECT * FROM users");
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}有关可用配置文件和自定义配置选项,请参阅 cacheLife API 参考。
运行时数据和 use cache 不能在同一作用域中使用。但是,您可以从运行时 API 中提取值并将它们作为参数传递给缓存函数。
import { cookies } from "next/headers";
import { Suspense } from "react";
export default function Page() {
// Page itself creates the dynamic boundary
return (
<Suspense fallback={<div>Loading...</div>}>
<ProfileContent />
</Suspense>
);
}
// Component (not cached) reads runtime data
async function ProfileContent() {
const session = (await cookies()).get("session")?.value;
return <CachedContent sessionId={session} />;
}
// Cached component/function receives data as props
async function CachedContent({ sessionId }: { sessionId: string }) {
"use cache";
// sessionId becomes part of cache key
const data = await fetchUserData(sessionId);
return <div>{data}</div>;
}在请求时,如果未找到匹配的缓存条目,CachedContent 将执行,并为将来的请求存储结果。
在 use cache 作用域内,非确定性操作在预渲染期间执行。当您希望将相同的渲染输出提供给所有用户时,这很有用:
export default async function Page() {
"use cache";
// Execute once, then cached for all requests
const random = Math.random();
const random2 = Math.random();
const now = Date.now();
const date = new Date();
const uuid = crypto.randomUUID();
const bytes = crypto.getRandomValues(new Uint8Array(16));
return (
<div>
<p>
{random} and {random2}
</p>
<p>{now}</p>
<p>{date.getTime()}</p>
<p>{uuid}</p>
<p>{bytes}</p>
</div>
);
}所有请求都将收到包含相同随机数、时间戳和 UUID 的路由,直到缓存被重新验证。
使用 cacheTag 标记缓存数据,并在变动后使用 Server Actions 中的 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 参考。
您缓存的内容应取决于您希望 UI 加载状态是什么。如果数据不依赖于运行时数据,并且您可以接受在一段时间内为多个请求提供缓存值,请使用 use cache 和 cacheLife 来描述该行为。
对于具有更新机制的内容管理系统,请考虑使用具有更长缓存持续时间的标签,并依赖 revalidateTag 将静态初始 UI 标记为可重新验证。这种模式允许您提供快速的缓存响应,同时在内容实际更改时仍然更新内容,而不是抢先使缓存过期。
这是一个完整的示例,展示了静态内容、缓存的动态内容和流式动态内容在单个页面上协同工作:
import { Suspense } from "react";
import { cookies } from "next/headers";
import { cacheLife } from "next/cache";
import Link from "next/link";
export default function BlogPage() {
return (
<>
{/* Static content - prerendered automatically */}
<header>
<h1>Our Blog</h1>
<nav>
<Link href="/">Home</Link> |{" "}
<Link href="/about">About</Link>
</nav>
</header>
{/* Cached dynamic content - included in the static shell */}
<BlogPosts />
{/* Runtime dynamic content - streams at request time */}
<Suspense fallback={<p>Loading your preferences...</p>}>
<UserPreferences />
</Suspense>
</>
);
}
// Everyone sees the same blog posts (revalidated every hour)
async function BlogPosts() {
"use cache";
cacheLife("hours");
const res = await fetch("https://api.vercel.app/blog");
const posts = await res.json();
return (
<section>
<h2>Latest Posts</h2>
<ul>
{posts.slice(0, 5).map((post: any) => (
<li key={post.id}>
<h3>{post.title}</h3>
<p>
By {post.author} on {post.date}
</p>
</li>
))}
</ul>
</section>
);
}
// Personalized per user based on their cookie
async function UserPreferences() {
const theme = (await cookies()).get("theme")?.value || "light";
const favoriteCategory = (await cookies()).get("category")?.value;
return (
<aside>
<p>Your theme: {theme}</p>
{favoriteCategory && <p>Favorite category: {favoriteCategory}</p>}
</aside>
);
}在预渲染期间,从 API 获取的头部(静态)和博客文章(使用 use cache 缓存)都将成为静态外壳的一部分,同时还有用户偏好的回退 UI。
当用户访问页面时,他们会立即看到这个带有头部和博客文章的预渲染外壳。只有个性化偏好需要在请求时流式传输,因为它们依赖于用户的 cookie。这确保了快速的初始页面加载,同时仍然提供个性化内容。
您可以通过在 Next 配置文件中添加 cacheComponents 选项来启用缓存组件(包括 PPR):
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;/** @type {import('next').NextConfig} */
const nextConfig = {
cacheComponents: true,
};
module.exports = nextConfig;须知: 启用缓存组件(Cache Components)后,GET 路由处理程序遵循与页面相同的预渲染模型。有关详细信息,请参阅带有缓存组件的路由处理程序。
当 cacheComponents 标志启用时,Next.js 使用 React 的 <Activity> 组件在客户端导航期间保留组件状态。
当您离开当前路由时,Next.js 不会卸载之前的路由,而是将其 Activity 模式设置为 "hidden"。这意味着:
这种行为通过在用户来回导航路由时保持 UI 状态(表单输入或展开的部分)来改善导航体验。
须知:Next.js 使用启发式算法来保留几个最近访问过的路由为 "hidden",而较旧的路由则从 DOM 中移除以防止过度增长。
当缓存组件(Cache Components)启用时,一些路由段配置选项不再需要或受支持:
dynamic = "force-dynamic"不再需要。 所有页面默认都是动态的。
// Before - No longer needed
export const dynamic = "force-dynamic";
export default function Page() {
return <div>...</div>;
}// After - Just remove it
export default function Page() {
return <div>...</div>;
}dynamic = "force-static"首先将其移除。当在开发和构建时检测到未处理的动态或运行时数据访问时,Next.js 会引发错误。否则,预渲染步骤会自动提取静态 HTML 外壳。
对于动态数据访问,请在数据访问附近添加 use cache,并使用较长的 cacheLife(例如 'max')以保持缓存行为。如果需要,可以将其添加到页面或布局的顶部。
对于运行时数据访问(cookies()、headers() 等),错误会指示您使用 Suspense 包装。由于您最初使用的是 force-static,因此必须移除运行时数据访问以防止任何请求时的工作。
// Before
export const dynamic = "force-static";
export default async function Page() {
const data = await fetch("https://api.example.com/data");
return <div>...</div>;
}import { cacheLife } from "next/cache";
// After - Use 'use cache' instead
export default async function Page() {
"use cache";
cacheLife("max");
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>;
}runtime = 'edge'不受支持。 缓存组件(Cache Components)需要 Node.js 运行时,并将与 Edge Runtime 抛出错误。