数据获取模式深度对比
Next.js 16 的数据获取经历了重大变化——fetch 缓存默认不再缓存、React
cache()去重请求、新的'use cache'指令取代旧的缓存语义。本章深入讲清每种数据获取模式的原理、缓存行为、以及在 AI SaaS 中的最佳选择。
1. 数据获取全景
1.1 Next.js 16 的缓存语义变化
重大变化
Next.js 15+ 对缓存策略做了根本性调整:
fetch()默认不缓存(之前默认缓存)GETRoute Handlers 默认不缓存(之前默认缓存)- 客户端路由缓存 默认不缓存(之前缓存 30s)
设计哲学从"默认缓存,手动退出"变为"默认动态,手动开启缓存"。
1.2 数据获取方式概览
| 方式 | 位置 | 缓存 | 适用场景 |
|---|---|---|---|
Server Component async/await | 服务端 | 可配置 | 页面初始数据 |
fetch + 缓存选项 | 服务端 | 可配置 | 外部 API 调用 |
unstable_cache | 服务端 | 有 | 数据库查询缓存 |
'use cache' | 服务端 | 有 | 新缓存指令(实验性) |
React cache() | 服务端 | 请求级去重 | 同一请求内多组件共享 |
| TanStack Query | 客户端 | 有 | 客户端交互数据 |
useSWR | 客户端 | 有 | 简单客户端数据 |
2. Server Component 直接获取数据
2.1 基础模式
Server Component 中可以直接 await 任何异步操作:
// app/(dashboard)/chat/page.tsx
export default async function ChatPage() {
// 直接查询数据库 — 无需 API 路由
const chats = await db.query.chats.findMany({
where: eq(chats.userId, userId),
orderBy: desc(chats.updatedAt),
limit: 50,
})
return <ChatList chats={chats} />
}2.2 外部 API 调用
export default async function WeatherWidget() {
const res = await fetch('https://api.weather.com/current?city=shanghai')
const weather = await res.json()
return <div>温度:{weather.temp}°C</div>
}2.3 错误处理
export default async function ChatPage() {
try {
const chats = await getChats()
return <ChatList chats={chats} />
} catch (error) {
// 不要在这里 catch redirect/notFound
console.error('Failed to fetch chats:', error)
return <ErrorMessage message="加载对话失败" />
}
}更推荐使用 error.tsx 边界来处理错误——它提供自动恢复能力。
3. fetch 缓存机制
3.1 默认行为(不缓存)
// Next.js 16:默认不缓存,等价于 cache: 'no-store'
const res = await fetch('https://api.example.com/data')3.2 显式开启缓存
// 强制缓存(构建时获取,永不更新)
const res = await fetch('https://api.example.com/data', {
cache: 'force-cache',
})
// 定时重验证(ISR 模式)
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }, // 1 小时后重验证
})
// 基于标签的重验证
const res = await fetch('https://api.example.com/data', {
next: { tags: ['posts'] },
})
// 然后在 Server Action 中:revalidateTag('posts')3.3 缓存行为总结
// ❌ 不缓存(默认)
fetch(url)
fetch(url, { cache: 'no-store' })
// ✅ 缓存
fetch(url, { cache: 'force-cache' }) // 永久缓存
fetch(url, { next: { revalidate: 60 } }) // 60秒后重验证
fetch(url, { next: { tags: ['posts'] } }) // 按标签重验证4. React cache() — 请求级去重
4.1 问题场景
同一个请求中,多个 Server Component 需要相同的数据:
// layout.tsx 需要用户信息
export default async function Layout({ children }) {
const user = await getUser() // 查询 1
return <div><Header user={user} />{children}</div>
}
// page.tsx 也需要用户信息
export default async function Page() {
const user = await getUser() // 查询 2(重复!)
return <Profile user={user} />
}4.2 使用 cache() 去重
// lib/data.ts
import { cache } from 'react'
export const getUser = cache(async () => {
const session = await auth()
if (!session?.user) return null
return db.query.users.findFirst({
where: eq(users.id, session.user.id),
})
})// 现在多次调用只执行一次数据库查询
// layout.tsx
const user = await getUser() // 第一次:执行查询
// page.tsx
const user = await getUser() // 第二次:返回缓存结果(同一请求内)4.3 cache() 的作用域
cache() 的缓存范围 = 单次服务端请求
请求 A:getUser() → 查询 DB → 缓存
getUser() → 返回缓存 ✅
请求 B:getUser() → 重新查询 DB(请求 A 的缓存不共享)
getUser() → 返回缓存 ✅
cache() 不是跨请求缓存!它只是"同一请求内的去重"。
5. unstable_cache — 跨请求缓存
5.1 数据库查询缓存
fetch 的缓存只适用于 HTTP 请求。对于数据库查询,需要 unstable_cache:
import { unstable_cache } from 'next/cache'
export const getChatList = unstable_cache(
async (userId: string) => {
return db.query.chats.findMany({
where: eq(chats.userId, userId),
orderBy: desc(chats.updatedAt),
})
},
['chat-list'], // 缓存 key
{
tags: ['chats'], // 用于 revalidateTag
revalidate: 300, // 5 分钟后重验证
},
)5.2 常用模式
// 带用户隔离的缓存
export const getUserChats = unstable_cache(
async (userId: string) => {
return db.query.chats.findMany({
where: eq(chats.userId, userId),
})
},
['user-chats'],
{
tags: ['chats'],
revalidate: 60,
},
)
// 使用
const chats = await getUserChats(session.user.id)
// Server Action 中失效
'use server'
export async function createChat(title: string) {
await db.insert(chats).values({ title })
revalidateTag('chats') // 所有 getUserChats 的缓存都失效
}6. 'use cache' 指令(实验性)
6.1 基础用法
Next.js 16 引入的新缓存指令,比 unstable_cache 更简洁:
// next.config.ts
const nextConfig: NextConfig = {
experimental: {
useCache: true,
},
}// 函数级缓存
async function getProducts() {
'use cache'
return db.query.products.findMany()
}
// 带重验证时间
async function getProducts() {
'use cache'
cacheLife('hours') // 预定义的缓存时长
return db.query.products.findMany()
}
// 带标签
async function getProducts() {
'use cache'
cacheTag('products')
return db.query.products.findMany()
}6.2 组件级缓存
async function ProductList() {
'use cache'
cacheLife('minutes')
const products = await db.query.products.findMany()
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
)
}6.3 cacheLife 预定义时长
// next.config.ts 中配置
const nextConfig = {
experimental: {
cacheLife: {
seconds: { stale: 10, revalidate: 5 },
minutes: { stale: 300, revalidate: 60 },
hours: { stale: 3600, revalidate: 900 },
days: { stale: 86400, revalidate: 3600 },
weeks: { stale: 604800, revalidate: 86400 },
},
},
}7. 并行与顺序数据获取
7.1 顺序获取(瀑布问题)
// ❌ 瀑布式获取 — 总耗时 = 200 + 300 + 100 = 600ms
export default async function DashboardPage() {
const user = await getUser() // 200ms
const chats = await getChats(user.id) // 300ms(等 user 完成后才开始)
const stats = await getStats(user.id) // 100ms(等 chats 完成后才开始)
return <Dashboard user={user} chats={chats} stats={stats} />
}7.2 并行获取
// ✅ 并行获取 — 总耗时 = max(200, 300, 100) = 300ms
export default async function DashboardPage() {
const user = await getUser() // 这个必须先获取(后面依赖它)
// chats 和 stats 并行获取
const [chats, stats] = await Promise.all([
getChats(user.id), // 300ms
getStats(user.id), // 100ms — 同时开始!
])
return <Dashboard user={user} chats={chats} stats={stats} />
}7.3 Suspense 并行(最优方案)
// ✅✅ Suspense 并行 — 每个部分独立加载,互不阻塞
export default async function DashboardPage() {
return (
<div className="grid grid-cols-2 gap-4">
<Suspense fallback={<Skeleton />}>
<ChatSection />
</Suspense>
<Suspense fallback={<Skeleton />}>
<StatsSection />
</Suspense>
</div>
)
}
async function ChatSection() {
const session = await auth()
const chats = await getChats(session.user.id) // 独立获取
return <ChatList chats={chats} />
}
async function StatsSection() {
const session = await auth() // cache() 去重,不会重复查询
const stats = await getStats(session.user.id) // 独立获取
return <StatsCard stats={stats} />
}7.4 对比
| 模式 | 总耗时 | 用户体验 | 代码复杂度 |
|---|---|---|---|
| 顺序获取 | A + B + C | 全部完成才显示 | 低 |
| Promise.all | max(A, B, C) | 全部完成才显示 | 中 |
| Suspense 并行 | 每个独立 | 渐进显示 | 中 |
8. 数据获取模式选择
8.1 AI SaaS 场景速查
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 对话列表(首屏) | Server Component + Suspense | 服务端直查,流式加载 |
| 对话消息(实时) | TanStack Query + 轮询 | 需要客户端实时更新 |
| 用户信息(多组件共用) | React cache() | 请求级去重 |
| 知识库列表(偶尔变化) | unstable_cache + revalidateTag | 跨请求缓存,按需失效 |
| 定价页(少更新) | ISR revalidate | 定时刷新 |
| 仪表盘多面板 | Suspense 并行 | 独立加载,互不阻塞 |
| Token 消耗图表 | TanStack Query | 客户端交互(时间范围切换) |
8.2 决策流程
数据在哪里获取?
├── 首屏展示 → Server Component async/await
│ ├── 需要跨请求缓存? → unstable_cache / 'use cache'
│ ├── 多组件共享? → React cache()
│ └── 多区域独立加载? → Suspense 并行
└── 用户交互后获取 → 客户端
├── 简单场景 → useSWR
└── 复杂场景(分页/乐观更新/轮询) → TanStack Query
本章小结
- fetch 默认不缓存:Next.js 16 的哲学是"默认动态,手动缓存"
- React
cache():请求级去重,同一请求内多次调用只执行一次 unstable_cache:跨请求缓存,配合revalidateTag精确失效'use cache':实验性新指令,更简洁的函数/组件级缓存- 并行获取:
Promise.all减少总耗时,Suspense 并行实现渐进显示 - 选择依据:首屏用 Server Component,交互后用 TanStack Query
下一章讲 TanStack Query 集成。