Next.js 16 Cache Components 完全指南

基于 Next.js 16.0.1 官方文档整理

目录

  1. 什么是 Cache Components
  2. 核心工作原理
  3. 使用 Suspense 边界
  4. 使用 use cache
  5. 启用 Cache Components
  6. 从旧版本迁移
  7. 实战示例
  8. 最佳实践
  9. 和 Next-intl 结合
  10. 常见错误与解决方案
  11. FAQ
  12. 参考资料

这边文档主要是 AI 总结+我补充实际遇到的问题,大部分是 AI 写的。

注意,文档里不包含use cache: private的内容,在写的时候本来官方文档里说依赖unstable_prefetch,但是后来一看这个内容又被移除了,不知道后续会不会再改,先不写了,以官方文档为准。API Reference > Directives > use cache: private

注意,现在的 16.0 还在不断变化中,还是等 16.1,16.2 再用吧。这东西太不稳了。

Cache components除了方便的 PPR+显式缓存,另外一个就是在框架层面(主要是 dev 和构建的时候),防止用户写出动态内容卡住整个页面加载的事,这在之前很容易写出来,网上也有很多批评的文章和视频,一看连loading.tsxSuspense都不会用。😂
现在官方强制了,也是件好事吧。

什么是 Cache Components

Cache Components 是 Next.js 16 中一种新的渲染和缓存方法,通过 Partial Prerendering (PPR) 提供细粒度的缓存控制,同时确保出色的用户体验。

核心关系

1
Cache Components = PPR + use cache
  • PPR 提供静态外壳和流式传输基础设施
  • use cache 让你在外壳中包含优化的动态输出

解决的问题

在开发动态应用时,你需要在两种方式之间权衡:

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

Cache Components 的解决方案

启用 Cache Components 后,Next.js 将所有路由默认视为动态。每个请求都使用最新可用数据渲染。但大多数页面由静态和动态部分组成,并非所有动态数据都需要在每次请求时从源获取。

Cache Components 允许你标记数据,甚至将 UI 的部分标记为可缓存,这会将它们与页面的静态部分一起包含在预渲染阶段中。

工作流程

当用户访问路由时:

  1. 服务器发送静态外壳:包含缓存内容,确保快速初始加载
  2. 动态部分显示 fallback:包裹在 Suspense 边界中的动态部分在外壳中显示 fallback UI
  3. 动态内容流式传输:只有动态部分渲染以替换其 fallback,并行流式传输
  4. 缓存的动态数据包含在外壳中:通过 use cache 缓存的原本动态的数据可以包含在初始外壳中

PPR 工作流程 - next.js官方图片

官方视频: Why PPR and how it works (10 分钟)


核心工作原理

Cache Components 提供三个关键工具来控制渲染:

1. Suspense for Runtime Data(运行时数据)

某些数据仅在实际用户发出请求时在运行时可用。

运行时 API 包括

使用方法:将使用这些 API 的组件包裹在 Suspense 边界中,以便页面的其余部分可以预渲染为静态外壳。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Suspense } from "react";
import { cookies } from "next/headers";

export default function Page() {
return (
<div>
{/* 静态部分 - 立即显示 */}
<header>My App</header>

{/* 动态部分 - 需要 Suspense */}
<Suspense fallback={<div>Loading user...</div>}>
<UserInfo />
</Suspense>
</div>
);
}

async function UserInfo() {
const userId = (await cookies()).get("userId")?.value;
const user = await db.users.findUnique({ where: { id: userId } });
return <div>Welcome, {user.name}</div>;
}

2. Suspense for Dynamic Data(动态数据)

动态数据如 fetch 调用或数据库查询可能在请求之间发生变化,但不是用户特定的。

动态数据模式包括

使用方法:将使用这些的组件包裹在 Suspense 边界中以启用流式传输。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Suspense } from "react";

export default function Page() {
return (
<div>
<h1>Products</h1>
<Suspense fallback={<div>Loading products...</div>}>
<ProductList />
</Suspense>
</div>
);
}

async function ProductList() {
const products = await db.products.findMany();
return (
<ul>
{products.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}

3. Cached Data with use cache(缓存数据)

use cache 添加到任何 Server Component 以使其缓存并包含在预渲染的外壳中。

限制

  • ❌ 不能在缓存组件内使用运行时 API(cookies、headers 等)
  • ✅ 可以标记工具函数为 use cache 并从 Server Components 调用
1
2
3
4
5
export async function getProducts() {
"use cache";
const data = await db.query("SELECT * FROM products");
return data;
}

使用 Suspense 边界

React Suspense 边界让你定义当它包裹动态或运行时数据时使用什么 fallback UI。

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Suspense } from "react";

export default function Page() {
return (
<>
<h1>这会被预渲染</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>;
}

工作原理

  • 边界外的内容(包括 fallback UI)被预渲染为静态外壳
  • 边界内的内容在准备好时流式传输

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

注意:将组件包裹在 Suspense 中不会使其动态;你的 API 使用决定了这一点。Suspense 作为封装动态内容和启用流式传输的边界。

缺少 Suspense 边界时的错误

Cache Components 强制要求动态代码必须包裹在 Suspense 边界中。如果忘记,你会看到错误:

1
2
3
4
5
6
7
8
9
Uncached data was accessed outside of <Suspense>

This delays the entire page from rendering, resulting in a slow user
experience. Next.js uses this error to ensure your app loads instantly
on every navigation.

To fix this, you can either:
- Wrap the component in a <Suspense> boundary
- Move the asynchronous await into a Cache Component("use cache")

修复方法

  1. 添加 Suspense 边界(推荐)
1
2
3
4
5
6
7
export default function Page() {
return (
<Suspense fallback={<Loading />}>
<DynamicComponent />
</Suspense>
);
}
  1. 使用 use cache 缓存工作
1
2
3
4
5
async function DynamicComponent() {
"use cache";
const data = await fetch("...");
return <div>{data}</div>;
}

流式传输工作原理

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

流式传输示意图 - next.js官方图片

通过部分预渲染,初始 UI 可以立即发送到浏览器,而动态部分则在渲染时流式传输。这减少了 UI 显示时间,并可能减少总请求时间。

流式传输并行化 - next.js官方图片

为了减少网络开销,完整响应(包括静态 HTML 和流式动态部分)在单个 HTTP 请求中发送。这避免了额外的往返,并提高了初始加载和整体性能。


使用 use cache

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

基本用法

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

1
2
3
4
5
6
7
8
9
import { cacheLife } from "next/cache";

export default async function Page() {
"use cache";
cacheLife("hours");

const data = await fetch("https://api.example.com/data");
return <div>{/* 渲染数据 */}</div>;
}

cacheLife 预定义配置

Profile Use Case stale revalidate expire
'default' Standard content 5 minutes 15 minutes 1 year
'seconds' Real-time data 30 seconds 1 second 1 minute
'minutes' Frequently updated content 5 minutes 1 minute 1 hour
'hours' Content updated multiple times per day 5 minutes 1 hour 1 day
'days' Content updated daily 5 minutes 1 day 1 week
'weeks' Content updated weekly 5 minutes 1 week 30 days
'max' Stable content that rarely changes 5 minutes 30 days 1 year

expire 要大于 stale,stale 不要小于 30s(不然缓存没意义)。

自定义配置

1
2
3
4
5
6
7
8
9
10
11
12
13
import { cacheLife } from "next/cache";

async function CustomCache() {
"use cache";
cacheLife({
stale: 60, // 60 秒内视为新鲜
revalidate: 300, // 300 秒后后台重新验证
expire: 3600, // 3600 秒后过期
});

const data = await fetch("https://api.example.com/data");
return data;
}

构建时与运行时行为

use cache at build time(构建时)

当在 layout 或 page 顶部使用 use cache 时,路由段会被预渲染,允许之后重新验证。

1
2
3
4
5
6
7
8
9
10
11
// app/products/page.tsx
"use cache";
export default async function ProductsPage() {
cacheLife("hours");

const products = await fetch("https://api.example.com/products").then((res) =>
res.json()
);

return <div>{/* 渲染产品 */}</div>;
}

特点

  • ✅ 路由段在构建时预渲染
  • ✅ 可以后续 revalidated
  • 不能与运行时数据一起使用(cookies、headers)

重要限制

1
2
3
4
5
6
"use cache";
// ❌ 错误:use cache 不能访问运行时数据
export default async function Page() {
const userId = (await cookies()).get("userId")?.value; // 运行时错误!
return <div>{userId}</div>;
}

注意:如果需要缓存依赖 cookies、headers 或 search params 的内容,请使用 'use cache: private' 代替。

use cache at runtime(运行时)

当应用运行时,use cache 在服务器和客户端的行为:

服务器端

  • 单个组件或函数的缓存条目会缓存在内存中
  • 多个请求可以共享这些缓存条目

客户端

  • 从服务器缓存返回的任何内容会存储在浏览器内存中
  • 持续整个会话或直到重新验证
  • 页面刷新或导航离开会清除缓存

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 服务器端缓存的工具函数
async function getCachedProducts() {
"use cache";
cacheLife("hours");

return await db.products.findMany();
}

export default async function Page() {
// 第一次调用:查询数据库 → 缓存在服务器内存
const products = await getCachedProducts();

// 后续请求:直接从服务器内存缓存返回
// 客户端收到的数据也会缓存在浏览器内存中
return <div>{/* 渲染产品 */}</div>;
}

缓存生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
构建时:
┌─────────────────────────────────────┐
│ 1. 预渲染路由段
│ 2. 生成静态 HTML
│ 3. 缓存结果供后续请求使用
└─────────────────────────────────────┘

运行时(服务器):
┌─────────────────────────────────────┐
│ 请求 1 → 执行函数 → 缓存结果(内存)
│ 请求 2 → 命中缓存 → 返回缓存结果
│ 请求 3 → 命中缓存 → 返回缓存结果
│ ...直到重新验证或过期
└─────────────────────────────────────┘

运行时(客户端):
┌─────────────────────────────────────┐
│ 1. 接收服务器缓存的数据
│ 2. 存储在浏览器内存
│ 3. 导航时复用(同一会话)
│ 4. session过期或者revalidated → 清除
└─────────────────────────────────────┘

使用限制

1. 参数必须可序列化

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

2. 可接受但不能内省不可序列化的值

你可以接受不可序列化的值作为参数,只要你不内省它们。但是,你可以返回它们。这允许像缓存组件接受 Server 或 Client Components 作为 children 的模式:

1
2
3
4
5
6
7
8
9
10
11
12
import { ReactNode } from "react";

export async function CachedWrapper({ children }: { children: ReactNode }) {
"use cache";
// 不要内省 children,只是传递它
return (
<div className="wrapper">
<header>Cached Header</header>
{children}
</div>
);
}

关键点:不可序列化的参数(如 JSX、函数)不会成为缓存键的一部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
async function getCached(
id: number, // ✅ 可序列化 → 包含在缓存键中
children: ReactNode, // ❌ 不可序列化 → 不包含在缓存键中
callback: () => void // ❌ 不可序列化 → 不包含在缓存键中
) {
"use cache";
return { id, content: <div>{children}</div> };
}

// 相同 id,不同 children → 缓存命中
const result1 = await getCached(1, <div>A</div>, () => {});
const result2 = await getCached(1, <div>B</div>, () => {});
// result1 和 result2 来自同一缓存(因为 id 相同)

3. 避免传递动态输入

除非你避免内省它们,否则不能将动态或运行时数据传递到 use cache 函数中。传递来自 cookies()headers() 或其他运行时 API 的值作为参数将导致错误,因为无法在预渲染时确定缓存键。

1
2
3
4
5
6
7
8
9
10
11
12
13
// ❌ 错误:传递运行时数据
async function BadExample() {
"use cache";
const userId = (await cookies()).get("userId")?.value; // 运行时错误!
return <div>{userId}</div>;
}

// ✅ 正确:不在缓存中使用运行时 API
async function GoodExample() {
// 无 'use cache'
const userId = (await cookies()).get("userId")?.value;
return <div>{userId}</div>;
}

标记和重新验证

使用 cacheTag 标记缓存数据,并在突变后使用 updateTagrevalidateTag 重新验证。

使用 updateTag(立即更新)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"use server";

import { cacheTag, updateTag } from "next/cache";

export async function getCart() {
"use cache";
cacheTag("cart");
// 获取数据
}

export async function updateCart(itemId: string) {
"use server";
// 使用 itemId 写入数据
// 更新用户购物车
updateTag("cart"); // 立即使缓存失效
}

使用场景

  • ✅ 需要用户立即看到更新(如购物车)
  • ✅ Read-your-own-writes 模式
  • ✅ Server Actions 专用

使用 revalidateTag(后台重新验证)

当你想要仅使正确标记的缓存条目失效并采用 stale-while-revalidate 行为时使用 revalidateTag。这对于可以容忍最终一致性的静态内容是理想的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"use server";

import { cacheTag, revalidateTag } from "next/cache";

export async function getPosts() {
"use cache";
cacheTag("posts");
// 获取数据
}

export async function createPost(post: FormData) {
"use server";
// 使用 FormData 写入数据
revalidateTag("posts", "max"); // 后台重新验证
}

使用场景

  • ✅ 可以容忍短暂过期数据(如博客文章、CMS 内容)
  • ✅ 避免”惊群效应”(多个请求同时触发数据重新生成)
  • ✅ Server Actions + Route Handlers

为什么推荐使用 'max' 配置

revalidateTag(tag, 'max') 使用 Stale-While-Revalidate (SWR) 策略:

  1. 调用 revalidateTag
  2. 缓存标记为 “stale”(过期但仍可用)
  3. 下一个请求:
    • ✅ 立即返回过期缓存(快速响应)
    • 🔄 同时后台触发重新验证
  4. 再下一个请求:
    • ✅ 返回更新后的新数据

优势

  • 用户不会遇到”等待数据重新生成”的延迟
  • 避免高流量时的并发数据库查询
  • 平滑的数据更新过渡

启用 Cache Components

next.config.ts 中添加 cacheComponents 选项:

1
2
3
4
5
6
7
8
// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
cacheComponents: true,
};

export default nextConfig;
1
2
3
4
5
6
7
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
cacheComponents: true,
};

module.exports = nextConfig;

当启用 cacheComponents 标志时,Next.js 使用 React 的 <Activity> 组件在客户端导航期间保留组件状态。

工作方式:

  • 状态保留:导航离开时不卸载前一个路由,而是设置 Activity 模式为 "hidden"
  • 导航回退:导航回来时,前一个路由及其状态完整保留
  • 效果清理:路由隐藏时清理效果,再次可见时重新创建

好处:通过在用户前后导航时维护 UI 状态(表单输入、展开的部分)来改善导航体验。

注意:Next.js 使用启发式方法保持几个最近访问的路由为 "hidden",而较旧的路由从 DOM 中移除以防止过度增长。


从旧版本迁移

启用 Cache Components 后,几个 Route Segment Config 选项不再需要或不受支持。以下是变化和迁移方法:

1. dynamic = "force-dynamic"

不再需要。启用 Cache Components 后,所有页面默认是动态的,因此此配置不必要。

1
2
3
4
5
6
// ❌ 之前 - 不再需要
export const dynamic = "force-dynamic";

export default function Page() {
return <div>...</div>;
}
1
2
3
4
// ✅ 之后 - 直接删除,页面默认是动态的
export default function Page() {
return <div>...</div>;
}

2. dynamic = "force-static"

use cache 替代。你必须为关联路由的每个 Layout 和 Page 添加 use cache

16.0.1 版本实际测试发现cache-control变成no-store了,关闭 cacheComponent 会出现 s-maxage 和 swr,行为不一致。

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

1
2
3
4
5
6
7
// ❌ 之前
export const dynamic = "force-static";

export default async function Page() {
const data = await fetch("https://api.example.com/data");
return <div>...</div>;
}
1
2
3
4
5
6
// ✅ 之后 - 使用 'use cache' 替代
export default async function Page() {
"use cache";
const data = await fetch("https://api.example.com/data");
return <div>...</div>;
}

3. revalidate

cacheLife 替代。使用 cacheLife 函数定义缓存持续时间,而不是路由段配置。

1
2
3
4
5
6
// ❌ 之前
export const revalidate = 3600; // 1 小时

export default async function Page() {
return <div>...</div>;
}
1
2
3
4
5
6
7
8
// ✅ 之后 - 使用 cacheLife
import { cacheLife } from "next/cache";

export default async function Page() {
"use cache";
cacheLife("hours");
return <div>...</div>;
}

4. fetchCache

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

1
2
// ❌ 之前
export const fetchCache = "force-cache";
1
2
3
4
5
6
// ✅ 之后 - 使用 'use cache' 控制缓存行为
export default async function Page() {
"use cache";
// 这里的所有 fetch 都被缓存
return <div>...</div>;
}

5. runtime = 'edge'

不支持。Cache Components 需要 Node.js 运行时,使用 Edge Runtime 会抛出错误。


实战示例

示例 1: 动态 API 使用

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

1
2
3
4
5
6
7
// app/user.tsx
import { cookies } from "next/headers";

export async function User() {
const session = (await cookies()).get("session")?.value;
return <div>User: {session}</div>;
}

页面组件(需要 Suspense):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// app/page.tsx
import { Suspense } from "react";
import { User } from "./user";

export default function Page() {
return (
<section>
<h1>这会被预渲染</h1>
<Suspense fallback={<div>Loading user...</div>}>
<User />
</Suspense>
</section>
);
}

示例 2: 传递动态 Props

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// app/page.tsx
import { Table, TableSkeleton } from "./table";
import { Suspense } from "react";

export default function Page({
searchParams,
}: {
searchParams: Promise<{ sort: string }>;
}) {
return (
<section>
<h1>这会被预渲染</h1>
<Suspense fallback={<TableSkeleton />}>
<Table searchParams={searchParams.then((search) => search.sort)} />
</Suspense>
</section>
);
}

Table 组件

1
2
3
4
5
6
// app/table.tsx
export async function Table({ sortPromise }: { sortPromise: Promise<string> }) {
const sort = (await sortPromise) === "true";
// 访问值使组件动态,但页面的其余部分会被预渲染
return <div>{/* 渲染表格 */}</div>;
}

示例 3: Route Handlers with Cache Components

GET Route Handlers 遵循与应用中正常 UI 路由相同的模型。它们默认是动态的,可以在确定性时预渲染,你可以使用 use cache 在缓存响应中包含更多动态数据。

动态示例(每次请求返回不同数字):

1
2
3
4
5
6
// app/api/random-number/route.ts
export async function GET() {
return Response.json({
randomNumber: Math.random(),
});
}

静态示例(在构建时预渲染):

1
2
3
4
5
6
// app/api/project-info/route.ts
export async function GET() {
return Response.json({
projectName: "Next.js",
});
}

缓存动态数据示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// app/api/products/route.ts
import { cacheLife } from "next/cache";

export async function GET() {
const products = await getProducts();
return Response.json(products);
}

async function getProducts() {
"use cache";
cacheLife("hours"); // 最多每小时查询一次数据库

return await db.query("SELECT * FROM products");
}

注意

  • use cache 不能直接在 Route Handler 主体中使用;提取到辅助函数
  • 缓存响应根据 cacheLife 在新请求到达时重新验证
  • 使用运行时 API 如 cookies()headers(),或调用 connection(),始终推迟到请求时(无预渲染)

示例 4: 完整电商页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// app/products/[id]/page.tsx
import { Suspense } from "react";
import { cacheLife, cacheTag } from "next/cache";
import { cookies } from "next/headers";

export default function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
return (
<div>
{/* 静态导航 - 立即显示 */}
<nav>
<a href="/">Home</a>
<a href="/products">Products</a>
</nav>

{/* 产品信息 - 公共缓存,1 小时 */}
<Suspense fallback={<ProductSkeleton />}>
<ProductInfo params={params} />
</Suspense>

{/* 用户购物车 - 动态,无缓存 */}
<Suspense fallback={<CartSkeleton />}>
<UserCart />
</Suspense>

{/* 推荐商品 - 公共缓存,1 天 */}
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations params={params} />
</Suspense>
</div>
);
}

// 产品信息 - 缓存 1 小时
async function ProductInfo({ params }: { params: Promise<{ id: string }> }) {
"use cache";
cacheLife("hours");
cacheTag("products");

const { id } = await params;
const product = await db.products.findUnique({ where: { id } });

return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>Price: ${product.price}</p>
</div>
);
}

// 用户购物车 - 动态,每次请求获取
async function UserCart() {
const userId = (await cookies()).get("userId")?.value;
const cart = await db.carts.findUnique({
where: { userId },
include: { items: true },
});

return (
<div>
<h2>Your Cart</h2>
<p>{cart.items.length} items</p>
</div>
);
}

// 推荐商品 - 缓存 1 天
async function Recommendations({
params,
}: {
params: Promise<{ id: string }>;
}) {
"use cache";
cacheLife("days");

const { id } = await params;
const recommendations = await getRecommendations(id);

return (
<div>
<h2>You might also like</h2>
<ul>
{recommendations.map((r) => (
<li key={r.id}>{r.name}</li>
))}
</ul>
</div>
);
}

最佳实践

1. 根据数据特性选择策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ✅ 公共、变化不频繁的数据 → use cache
async function getCategories() {
"use cache";
cacheLife("days");
cacheTag("categories");
return await db.categories.findMany();
}

// ✅ 用户特定数据 → 动态渲染(无缓存)
async function getUserProfile() {
const userId = (await cookies()).get("userId")?.value;
return await db.users.findUnique({ where: { id: userId } });
}

// ✅ 实时数据 → 动态渲染 + connection()
async function getLiveData() {
await connection();
return await fetch("https://api.example.com/live");
}

2. 合理使用 Suspense 嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export default function Page() {
return (
<div>
{/* 外层:页面级 Suspense */}
<Suspense fallback={<PageSkeleton />}>
<PageContent />
</Suspense>
</div>
);
}

async function PageContent() {
return (
<div>
<StaticHeader />

{/* 内层:组件级 Suspense,细粒度控制 */}
<Suspense fallback={<UserSkeleton />}>
<UserInfo />
</Suspense>

<Suspense fallback={<ProductsSkeleton />}>
<ProductList />
</Suspense>
</div>
);
}

3. 缓存粒度策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// ✅ 好:细粒度缓存
async function ProductCard({ id }: { id: string }) {
"use cache";
cacheLife("hours");
cacheTag("products", `product-${id}`);

const product = await db.products.findUnique({ where: { id } });
return <div>{product.name}</div>;
}

export default function ProductList({ ids }: { ids: string[] }) {
return (
<div>
{ids.map((id) => (
<ProductCard key={id} id={id} />
))}
</div>
);
}

// ❌ 差:粗粒度缓存(不灵活)
export default async function ProductList({ ids }: { ids: string[] }) {
"use cache"; // 整个列表作为一个缓存

const products = await db.products.findMany({
where: { id: { in: ids } },
});

return (
<div>
{products.map((p) => (
<div key={p.id}>{p.name}</div>
))}
</div>
);
}

为什么细粒度更好

  • 单个产品更新时,只需要使一个缓存失效
  • 不同产品可以有不同的缓存策略
  • 更容易调试和维护

4. 多级缓存标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async function ProductDetails({ id }: { id: string }) {
"use cache";
cacheLife("hours");

// 使用多个标签便于不同级别的失效
cacheTag(
"products", // 所有产品
`product-${id}`, // 特定产品
`category-electronics` // 产品类别
);

const product = await db.products.findUnique({ where: { id } });
return <div>{product.name}</div>;
}

// 失效策略:
// revalidateTag(`product-${id}`, 'max') // 仅失效特定产品
// revalidateTag('category-electronics', 'max') // 失效整个类别
// revalidateTag('products', 'max') // 失效所有产品

5. generateStaticParams 最佳实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ✅ 推荐:预生成热门内容,其余按需生成
export async function generateStaticParams() {
// 只预生成前 20 个热门产品
const topProducts = await db.products
.orderBy("views", "desc")
.limit(20)
.select("id");

return topProducts.map((p) => ({ id: p.id }));
}

export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
"use cache";
cacheLife("hours");
cacheTag("products");

const { id } = await params;
const product = await db.products.findUnique({ where: { id } });

return <div>{product.name}</div>;
}

策略说明

  • 低基数参数(如分类、语言):预生成所有值
  • 高基数参数(如产品 ID):只预生成热门值
  • 按需生成 + 缓存 = ISR 效果

和 Next-intl 结合

现在使用 next-intl 可能会导致报各种缺少 Suspense 边界的错,这个的核心是在不指定 locale 的情况下,使用getTranslate,useTranslate,<Link>(next-intl 的 Link)会去读取 header(x-next-intl-locale),相当于这个库会自己去用 dynamic api。

要解决很简单,拿到 locale 之后塞到setRequestLocale里或者直接塞到各个的 locale 参数里。

这个和解决 static 渲染类似。

参考:
Add setRequestLocale to all relevant layouts and pages

常见错误与解决方案

错误 1: 缺少 Suspense 边界

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ❌ 错误
export default async function Page() {
const userId = (await cookies()).get("userId")?.value;
return <div>{userId}</div>;
}

// 错误信息:
// Uncached data was accessed outside of <Suspense>

// ✅ 解决方案 1: 添加 Suspense
export default function Page() {
return (
<Suspense fallback={<Loading />}>
<UserContent />
</Suspense>
);
}

async function UserContent() {
const userId = (await cookies()).get("userId")?.value;
return <div>{userId}</div>;
}

// ✅ 解决方案 2: 使用 use cache(如果适用)
export default async function Page() {
"use cache";
// 但注意:不能在 use cache 中使用 cookies()
}

错误 2: 在 use cache 中使用运行时 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ❌ 错误
async function Component() {
"use cache";
const userId = (await cookies()).get("userId")?.value; // 运行时错误!
return <div>{userId}</div>;
}

// ✅ 解决方案:移除 use cache,使用 Suspense
async function Component() {
// 无 'use cache'
const userId = (await cookies()).get("userId")?.value;
return <div>{userId}</div>;
}

export default function Page() {
return (
<Suspense fallback={<Loading />}>
<Component />
</Suspense>
);
}

错误 3: Route Segment Config 不兼容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ❌ 错误
export const revalidate = 60;
export const dynamic = "force-static";

// 错误信息:
// Route segment config "revalidate" is not compatible with
// `nextConfig.cacheComponents`. Please remove it.

// ✅ 解决方案
import { cacheLife } from "next/cache";

export default async function Page() {
"use cache";
cacheLife({ revalidate: 60 });
// ...
}

错误 4: 未 await params/searchParams

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ❌ 错误
export default function Page({ params }) {
const id = params.id; // 类型错误!params 是 Promise
}

// ✅ 解决方案
export default async function Page({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params; // 必须 await
return <div>Product: {id}</div>;
}

错误 5: 在 Route Handler 主体中使用 use cache

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ❌ 错误
export async function GET() {
"use cache"; // 不能直接在这里使用
return Response.json({ data: "..." });
}

// ✅ 解决方案:提取到辅助函数
async function getData() {
"use cache";
cacheLife("hours");
return await db.query("SELECT * FROM data");
}

export async function GET() {
const data = await getData();
return Response.json(data);
}

错误 6: 传递不可序列化的参数到 use cache

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ❌ 错误
async function cachedFunction(callback: () => void) {
"use cache";
callback(); // 错误:内省不可序列化的参数
}

// ✅ 解决方案 1:不内省,只传递
async function cachedWrapper({ children }: { children: ReactNode }) {
"use cache";
return <div>{children}</div>; // 只传递,不检查
}

// ✅ 解决方案 2:只接受可序列化参数
async function cachedFunction(data: { id: number; name: string }) {
"use cache";
return data;
}

FAQ

Q: Cache Components 是否取代 PPR?

A: 不。Cache Components 实现了 PPR 作为特性。旧的实验性 PPR 标志已被移除,但 PPR 仍然存在。

  • PPR 提供静态外壳和流式传输基础设施
  • use cache 让你在外壳中包含优化的动态输出

Q: 我应该首先缓存什么?

A: 你缓存的内容应该取决于你希望 UI 加载状态是什么。如果数据不依赖运行时数据,并且你可以接受在一段时间内为多个请求提供缓存值,请使用 use cachecacheLife 来描述该行为。

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

Q: 如何快速更新缓存内容?

A: 使用 cacheTag 标记你的缓存数据,然后触发 updateTagrevalidateTag

选择指南

场景 使用 行为
需要立即看到更新 updateTag 立即过期,同一请求内刷新
可以容忍短暂过期 revalidateTag Stale-while-revalidate,后台更新

Q: ISR 还支持吗?

A: 是的,ISR 功能仍然存在,但 API 已更改:

  • export const revalidate = 60
  • cacheLife({ revalidate: 60 })

使用 use cache + cacheLife + generateStaticParams 可以实现完整的 ISR 功能。

Q: 可以混用旧 API 和新 API 吗?

A: 不可以。启用 cacheComponents: true 后:

  • ❌ 不能使用 export const revalidate
  • ❌ 不能使用 export const dynamic
  • ❌ 不能使用 export const fetchCache
  • ✅ 必须使用 use cache + cacheLife

Q: Edge Runtime 支持吗?

A: 不支持。Cache Components 需要 Node.js 运行时。


参考资料

官方文档

关键 API 速查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 缓存指令
"use cache";

// 缓存配置
import { cacheLife, cacheTag } from "next/cache";
cacheLife("hours");
cacheLife({ stale: 60, revalidate: 300, expire: 3600 });
cacheTag("tag1", "tag2");

// 缓存失效
import { updateTag, revalidateTag } from "next/cache";
updateTag("tag"); // 立即失效(Server Actions)
revalidateTag("tag", "max"); // 后台重新验证(Server Actions + Route Handlers)

// 运行时 API
import { cookies, headers } from "next/headers";
import { connection } from "next/server";
await cookies();
await headers();
await connection();

// Suspense
import { Suspense } from "react";
<Suspense fallback={<Loading />}>
<DynamicComponent />
</Suspense>;

视频资源


总结

Cache Components 的核心理念

  1. Cache Components = PPR + use cache

    • PPR 是基础(静态外壳 + 流式传输)
    • use cache 是增强(显式缓存控制)
  2. 默认动态,选择性缓存

    • 与旧版本相反,现在默认都是动态的
    • 你决定什么需要缓存
  3. 三个关键工具

    • Suspense for runtime data(运行时数据)
    • Suspense for dynamic data(动态数据)
    • use cache for cached data(缓存数据)
  4. 从旧 API 迁移

    • 移除所有 Route Segment Config
    • 使用 use cache + cacheLife 替代
    • params/searchParams 现在是 Promise(你从 Next.js 15 来的这点就问题不大)
  5. 最佳实践

    • 根据数据特性选择策略
    • 细粒度缓存优于粗粒度
    • 合理使用 Suspense 嵌套
    • 多级缓存标签便于失效管理

Cache Components 让缓存行为更清晰、更可控,是构建高性能 Next.js 应用的强大工具。