数据获取模式深度对比

Next.js 16 的数据获取经历了重大变化——fetch 缓存默认不再缓存、React cache() 去重请求、新的 'use cache' 指令取代旧的缓存语义。本章深入讲清每种数据获取模式的原理、缓存行为、以及在 AI SaaS 中的最佳选择。

1. 数据获取全景

1.1 Next.js 16 的缓存语义变化

重大变化

Next.js 15+ 对缓存策略做了根本性调整:

  • fetch() 默认不缓存(之前默认缓存)
  • GET Route 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.allmax(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 集成。