Skip to main content

数据获取与缓存策略

面试题:Next.js 中 fetch 的缓存机制是什么?revalidatePath 和 revalidateTag 区别?React.cache 解决什么问题?

一、Next.js 15 的 fetch 扩展

Next.js 15 扩展了原生 fetch,增加了缓存控制能力。所有 Server Component 中的 fetch 行为都受 Next.js 缓存层管理

1.1 四种缓存策略

// ① 默认:永久缓存(force-cache)
const res = await fetch("https://api.example.com/posts");
// 结果会缓存到 .next/cache,下次 build/ISR 复用
// 适合:构建期数据、永久不变的数据

// ② 不缓存(no-store)
const res = await fetch("https://api.example.com/me", {
cache: "no-store",
});
// 每次请求都重新拉
// 适合:用户个性化数据、敏感数据

// ③ 时间窗口(revalidate)
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 60 },
});
// 60 秒内复用缓存,过期后台重新拉
// 适合:ISR 场景

// ④ 标签失效(tag-based)
const res = await fetch("https://api.example.com/posts", {
next: { tags: ["posts"] },
});
// 主动调用 revalidateTag("posts") 时才失效
// 适合:内容更新可控的场景

1.2 与原生 fetch 的区别

面试题:Next.js 的 fetch 和浏览器 fetch 有什么区别?

维度浏览器 fetchNext.js fetch
运行环境浏览器服务端(Node / Edge)
缓存位置浏览器 HTTP 缓存服务端持久缓存(.next/cache)
跨请求复用HTTP 304 / max-age直接复用,不发请求
额外配置{ next: { revalidate, tags } }

关键:Next.js 看到 Server Component 里调 fetch 时,会拦截到自己的缓存层,不会真的发到网络(如果命中缓存)。

二、四种渲染模式与 fetch 的关系

// app/posts/page.tsx

// 情况 1:默认行为 = 静态生成(SSG)
const res = await fetch("/api/posts");
// → build 时拉一次,缓存到 .next/cache

// 情况 2:no-store = 动态渲染(SSR)
const res = await fetch("/api/posts", { cache: "no-store" });
// → 每次请求都拉

// 情况 3:revalidate = ISR
const res = await fetch("/api/posts", { next: { revalidate: 60 } });
// → build 时拉一次,60s 后过期重新拉

// 情况 4:tags = 按需失效
const res = await fetch("/api/posts", { next: { tags: ["posts"] } });
// → 缓存永久生效,revalidateTag("posts") 才失效

Next.js 的渲染模式 = fetch 缓存策略的封装记住一个,能推四个

三、动态 API 触发动态渲染

// app/page.tsx
import { cookies, headers } from "next/headers";

export default async function Page() {
const cookieStore = await cookies(); // 15+ 异步
const token = cookieStore.get("token");

// 只要用了 cookies() / headers() 这类动态 API
// Next.js 自动把整个页面切换到「动态渲染」
// 即使你 fetch 不加 cache: "no-store"

return <div>{token?.value}</div>;
}

面试题:什么是「动态 API」? cookies() / headers() / searchParams / draftMode()调用任何一个,Next.js 自动放弃缓存,整个路由变动态渲染

四、revalidatePath vs revalidateTag

4.1 revalidatePath

按路径失效缓存

// app/actions/post.ts
"use server";

import { revalidatePath } from "next/cache";

export async function createPost(formData: FormData) {
await db.post.create({ data: { ... } });
revalidatePath("/posts"); // 失效 /posts 路由缓存
revalidatePath("/posts/[id]", "page"); // 失效动态路由
revalidatePath("/", "layout"); // 失效所有页面
}

场景:明确知道改了哪个路径。

4.2 revalidateTag

按标签失效缓存

// app/api/posts/route.ts
export async function GET() {
const data = await fetch("https://cms.example.com/posts", {
next: { tags: ["posts"] }, // 打标签
});
return Response.json(await data.json());
}

// app/actions/post.ts
"use server";
import { revalidateTag } from "next/cache";

export async function createPost(formData: FormData) {
await db.post.create({ data: { ... } });
revalidateTag("posts"); // 失效所有带 posts 标签的 fetch
}

场景:一个数据被多个路由使用,希望一改全更新

4.3 对比

维度revalidatePathrevalidateTag
粒度路径级标签级(可跨路径)
适用单页面更新共享数据多页面更新
配置简单(直接传路径)需提前给 fetch 打标签
影响单路由所有打该标签的 fetch

面试题:什么场景用 Tag? 多页面共享同一份数据(如「文章列表」数据被 /posts/tags/[tag]/authors/[id] 都用到),改一篇文章要让所有相关页面都更新,用 tag 一行解决

五、unstable_cache(数据级缓存)

如果数据不是通过 fetch 拿的(如数据库直查、Redis 调用),用 unstable_cache

import { unstable_cache } from "next/cache";

const getCachedPosts = unstable_cache(
async () => {
return await db.post.findMany(); // 同步数据源
},
["posts-key"], // 缓存 key
{
revalidate: 60, // 60s 过期
tags: ["posts"], // 标签
},
);

// 用法
export default async function Page() {
const posts = await getCachedPosts();
return <PostList posts={posts} />;
}

注意unstable_ 前缀表示 API 还在变,但 15+ 已经是稳定形态,生产可用。

六、React.cache(请求级去重)

6.1 问题场景

// lib/user.ts
export async function getUser(id: string) {
return await db.user.findUnique({ where: { id } });
}

// app/dashboard/page.tsx
export default async function Page() {
const user = await getUser("123"); // 第 1 次 DB 查询
return <UserProfile user={user} />;
}

// app/dashboard/layout.tsx
export default async function Layout({ children }) {
const user = await getUser("123"); // 第 2 次 DB 查询(重复!)
return <Shell user={user}>{children}</Shell>;
}

// UserProfile 组件
async function UserProfile({ user }) {
const orders = await getOrders(user.id); // 第 3 次
return <div>...</div>;
}

问题:同一个 getUser("123") 在一次请求中被调了 N 次,每次都打 DB

6.2 解决

// lib/user.ts
import { cache } from "react";

export const getUser = cache(async (id: string) => {
return await db.user.findUnique({ where: { id } });
});

效果

  • 同一个请求内 getUser("123") 只调一次
  • 不同请求间不共享缓存(每个请求独立)
  • 不需要传 key(自动用参数)

6.3 进阶:与 unstable_cache 配合

import { cache } from "react";
import { unstable_cache } from "next/cache";

export const getUser = cache(
unstable_cache(
async (id: string) => db.user.findUnique({ where: { id } }),
["user"],
{ revalidate: 3600, tags: ["user"] },
),
);

两层缓存

粒度失效
unstable_cache跨请求时间 / 标签
cache单次请求请求结束自动清理

性能:未命中第一层 → 第二层;都未命中 → 真打 DB。

七、客户端数据获取(TanStack Query)

虽然 Server Component 解决了很多问题,但客户端实时数据仍需要数据获取库:

"use client";

import { useQuery } from "@tanstack/react-query";

export function UserList() {
const { data, isLoading } = useQuery({
queryKey: ["users"],
queryFn: () => fetch("/api/users").then(r => r.json()),
staleTime: 30_000, // 30s 内不发请求
});

if (isLoading) return <Skeleton />;
return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

适用场景

  • 实时刷新的数据(如聊天、股票)
  • 客户端状态依赖服务端数据
  • 频繁交互(搜索、分页、过滤)

八、面试高频追问

Q1:revalidate 和 ISR 什么关系?

A:ISR = revalidate 时间窗口缓存。配置 revalidate = 60 就等同于 60s 的 ISR。

Q2:什么时候用 unstable_cache,什么时候用 React.cache?

A:

  • unstable_cache跨请求复用(持久化)
  • React.cache单次请求内去重(临时)
  • 两者可叠加(外层 unstable_cache 持久化,内层 cache 去重)

Q3:revalidateTag 失效的颗粒度是什么?

A:打到该 tag 的所有 fetch 缓存。粒度由打 tag 时决定,不区分页面、不区分路径

Q4:Server Action 里能 revalidate 吗?

A:可以,且最常见的用法就是 Server Action 改完数据后调 revalidatePath / revalidateTag,让 Client 自动看到新数据。

Q5:缓存命中率怎么监控?

A:Next.js 16+ 提供 cacheLifecacheTag 工具,配合 Vercel Observability / 自建 Prometheus。

九、决策流程图

需要数据?
├─ 是
│ ├─ 在 Server Component?
│ │ ├─ 是
│ │ │ ├─ 数据用 fetch 拿? → fetch + cache 配置
│ │ │ └─ 数据从 DB 拿? → unstable_cache + cache
│ │ └─ 否(Client Component)→ TanStack Query
│ └─ 用户实时交互(搜索/分页) → Client fetch + 缓存
└─ 否(纯静态)→ 直接写

十、相关文档