Next.js: 数据获取:fetch 与 RSC
最后更新:2026-08-26
RSC 中
fetch不再是浏览器的 fetch——它扩展了缓存层,让你以声明式的方式控制数据生命周期。
1. 你将学到
- Server Component 中
fetch(url, options)的自动缓存行为 - 三种缓存模式:
force-cache、no-store、revalidate next: { tags }与revalidateTag()按需重验证- 并行数据获取
Promise.all()避免瀑布流 - 串行瀑布流的识别与优化
2. 一个全栈开发者的真实故事
(1) 痛点:Dashboard 加载要 8 秒
Bob 是 TaskFlow 团队的技术主管。Dashboard 页面需要加载 5 个数据源:用户统计、项目总数、最近任务、活动日志、系统通知。最初代码写了 5 个串行的 await fetch(...),每个等待前一个完成——总耗时 2.1s + 1.8s + 1.5s + 0.9s + 1.7s = 8 秒。用户投诉页面"白屏太久"。更糟的是,每次刷新都重新请求 API,数据库压力飙升到 5,000 QPS。
(2) Next.js fetch 的解法
用
Promise.all()并行请求 +next: { revalidate: 60 }缓存 60 秒。
// app/dashboard/page.tsx
export default async function DashboardPage() {
const [users, projects, tasks, logs, notifs] = await Promise.all([
fetch('https://api.example.com/stats/users', { next: { revalidate: 60 } }),
fetch('https://api.example.com/stats/projects', { next: { revalidate: 60 } }),
fetch('https://api.example.com/stats/tasks', { next: { revalidate: 30 } }),
fetch('https://api.example.com/activity/logs', { cache: 'no-store' }),
fetch('https://api.example.com/notifications', { next: { revalidate: 10 } }),
]).then(responses => Promise.all(responses.map(r => r.json())))
return <DashboardView {...{ users, projects, tasks, logs, notifs }} />
}
(3) 收益
| 维度 | 优化前 | 优化后 |
|---|---|---|
| 页面加载时间 | 8 秒(串行) | 2.1 秒(并行) |
| 数据库 QPS | 5,000 | 83(缓存 60 秒) |
| 用户投诉 | 每天 12 起 | 0 起 |
| 代码行数 | 35 行(5 个独立 fetch) | 10 行 |
3. fetch 的三种缓存模式
Next.js 16 扩展了 Web fetch API,添加了三种缓存模式。所有 RSC 中的 fetch 都默认使用 force-cache(自动缓存),除非显式指定其他模式。
graph LR
A[RSC fetch] --> B{缓存模式}
B --> C[force-cache<br/>默认值]
B --> D[no-store<br/>每次刷新]
B --> E[revalidate:N<br/>时间窗口]
C --> F[Data Cache<br/>持久化存储]
D --> G[实时数据<br/>不缓存]
E --> H[N 秒内缓存<br/>过期后重新获取]
style C fill:#d4edda
style D fill:#f8d7da
style E fill:#fff3cd
| 模式 | 写法 | 行为 | 适用场景 |
|---|---|---|---|
force-cache(默认) |
fetch(url) 或 fetch(url, { cache: 'force-cache' }) |
只在构建/首次请求时获取,结果永久缓存 | 极少变化的数据(文档、静态配置) |
no-store |
fetch(url, { cache: 'no-store' }) |
每次请求都重新获取,不缓存 | 实时数据(用户信息、库存) |
revalidate:N |
fetch(url, { next: { revalidate: 60 } }) |
60 秒内缓存,过期后后台触发更新 | 半实时数据(新闻、排行榜) |
(1) force-cache 默认行为
如果不传任何 options,Next.js 会自动缓存 fetch 结果——相同的 URL 和 options 在构建期间只会请求一次。
// app/products/page.tsx — force-cache 默认
export default async function ProductsPage() {
const products = await fetch('https://api.example.com/products').then(r => r.json())
// 构建时获取一次,之后使用缓存
return <ProductList data={products} />
}
(2) no-store 动态数据
// app/profile/page.tsx — 每次请求都获取最新数据
export default async function ProfilePage() {
const user = await fetch('https://api.example.com/me', {
cache: 'no-store',
headers: { Authorization: `Bearer ${process.env.API_TOKEN}` }
}).then(r => r.json())
return <ProfileView user={user} />
}
(3) revalidate 时间窗口
// app/blog/[slug]/page.tsx — ISR 风格缓存
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await fetch(`https://cms.example.com/posts/${params.slug}`, {
next: { revalidate: 3600 } // 1 小时内使用缓存
}).then(r => r.json())
return <article><h1>{post.title}</h1><div>{post.content}</div></article>
}
▶ 示例:三种缓存模式对比(难度⭐)
// app/cache-demo/page.tsx
export default async function CacheDemoPage() {
const staticData = await fetch('http://worldtimeapi.org/api/timezone/Etc/UTC', {
cache: 'force-cache'
}).then(r => r.json())
const liveData = await fetch('http://worldtimeapi.org/api/timezone/Etc/UTC', {
cache: 'no-store'
}).then(r => r.json())
return (
<div>
<p>Static (force-cache): {staticData.datetime}</p>
<p>Live (no-store): {liveData.datetime}</p>
</div>
)
}
输出:
Static (force-cache): 2026-07-06T10:00:00.000Z ← 一直不变
Live (no-store): 2026-07-06T10:00:05.123Z ← 每次刷新都变
4. 按需重验证:tags 与 revalidateTag
next: { tags: [...] } 为 fetch 请求打上标签,随后通过 revalidateTag(tag) 在 Server Action 或 Route Handler 中按需刷新缓存。
sequenceDiagram
participant A as Server Action
participant Cache as Data Cache
participant DB as Database
A->>DB: 写入新数据(创建任务)
A->>Cache: revalidateTag('tasks')
Cache->>Cache: 清除 tags 匹配的所有缓存
Note over Cache: 下次 fetch 重新获取
| API | 用途 | 调用位置 |
|---|---|---|
next: { tags: ['tasks', 'projects'] } |
给 fetch 打标签 | fetch() 选项 |
revalidateTag('tasks') |
按标签清除所有相关缓存 | Server Action / Route Handler |
revalidatePath('/dashboard') |
按路径清除缓存 | Server Action / Route Handler |
▶ 示例:使用 tags 与 revalidateTag(难度⭐⭐)
// app/tasks/data.ts — 数据获取函数
export async function getTasks() {
return fetch('https://api.example.com/tasks', {
next: { tags: ['tasks'] }
}).then(r => r.json())
}
// app/tasks/actions.ts — Server Action 写入后刷新缓存
'use server'
import { revalidateTag } from 'next/cache'
export async function createTask(formData: FormData) {
const title = formData.get('title') as string
await fetch('https://api.example.com/tasks', {
method: 'POST',
body: JSON.stringify({ title, status: 'todo' })
})
revalidateTag('tasks') // 清除 tasks 标签的所有缓存
}
▶ 示例:revalidatePath 清除整页(难度⭐⭐)
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function publishArticle() {
await db.article.update({ where: { id: 1 }, data: { published: true } })
revalidatePath('/blog') // 刷新 /blog 页面
revalidatePath('/blog/[slug]') // 刷新所有文章详情
}
5. 并行数据获取与瀑布流避免
瀑布流(Waterfall)是性能头号杀手——每个 await 串行等待上一个完成。使用 Promise.all() 可以同时发起所有请求。
graph LR
subgraph "瀑布流(慢)"
A1[fetch A] --> A2[fetch B] --> A3[fetch C]
A1 -.- t1[2s]
A2 -.- t2[+2s = 4s]
A3 -.- t3[+2s = 6s]
end
subgraph "并行(快)"
B1[fetch A] -.- u1[2s]
C1[fetch B] -.- u2[2s]
D1[fetch C] -.- u3[2s]
B1 & C1 & D1 --> M[Promise.all<br/>总耗时 ~2s]
end
| 模式 | 写法 | 总耗时(每个 2s) | 适用场景 |
|---|---|---|---|
| 串行瀑布流 | await A; await B; await C |
~6s | 有依赖关系的请求 |
| 并行请求 | Promise.all([A, B, C]) |
~2s | 独立不相关的请求 |
| 分阶段并行 | const a = await A; const [b, c] = await Promise.all([B(a.id), C]) |
~4s | 部分依赖的请求 |
▶ 示例:串行瀑布流识别(难度⭐)
// app/waterfall/page.tsx — ❌ 串行瀑布流
export default async function WaterfallPage() {
const user = await fetch('https://api.example.com/user').then(r => r.json()) // 1s
const tasks = await fetch(`https://api.example.com/tasks?userId=${user.id}`).then(r => r.json()) // 等上面完成 + 2s = 3s
const details = await Promise.all(tasks.map(t =>
fetch(`https://api.example.com/tasks/${t.id}/details`).then(r => r.json()) // 等上面完成 + 2s = 5s
))
return <div>Total: ~5s</div>
}
▶ 示例:并行优化(难度⭐⭐)
// app/no-waterfall/page.tsx — ✅ 并行优化
export default async function NoWaterfallPage() {
// 阶段 1:并行获取用户和初始化数据
const [user, initialData] = await Promise.all([
fetch('https://api.example.com/user', { next: { revalidate: 10 } }).then(r => r.json()),
fetch('https://api.example.com/initial', { cache: 'no-store' }).then(r => r.json()),
])
// 阶段 2:依赖 user.id 的请求(仍有小瀑布,但已是最优)
const tasks = await fetch(`https://api.example.com/tasks?userId=${user.id}`).then(r => r.json())
return <div>Total: ~2s (1s + 1s parallel, then 1s)</div>
}
▶ 示例:Suspense 边界分割加载(难度⭐⭐⭐)
// app/suspense-demo/page.tsx — 每个独立区域用 Suspense 包裹
import { Suspense } from 'react'
export default function SuspenseDemoPage() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<div>Loading profile...</div>}>
<ProfileSection />
</Suspense>
<Suspense fallback={<div>Loading tasks...</div>}>
<TaskSection />
</Suspense>
</div>
)
}
async function ProfileSection() {
const user = await fetch('https://api.example.com/user', { cache: 'no-store' }).then(r => r.json())
return <div>Welcome, {user.name}</div>
}
async function TaskSection() {
const tasks = await fetch('https://api.example.com/tasks', { next: { revalidate: 30 } }).then(r => r.json())
return <ul>{tasks.map((t: any) => <li key={t.id}>{t.title}</li>)}</ul>
}
6. 完整示例:优化后的 Dashboard
// app/dashboard-optimized/page.tsx
import { Suspense } from 'react'
import { revalidateTag } from 'next/cache'
// ======== 数据函数 ========
const API = 'https://jsonplaceholder.typicode.com'
async function getData<T>(endpoint: string, options?: RequestInit): Promise<T> {
const res = await fetch(`${API}${endpoint}`, {
...options,
next: { tags: [endpoint.split('/')[1] ?? 'default'], ...(options as any)?.next },
})
if (!res.ok) throw new Error(`Failed to fetch ${endpoint}`)
return res.json()
}
// ======== 并行获取所有数据 ========
export default function DashboardOptimizedPage() {
return (
<div>
<h1>Optimized Dashboard</h1>
<div style={{ display: 'grid', gap: 16, gridTemplateColumns: '1fr 1fr' }}>
<Suspense fallback={<Skeleton label="Users" />}>
<DataCard title="Users" endpoint="/users" />
</Suspense>
<Suspense fallback={<Skeleton label="Posts" />}>
<DataCard title="Posts" endpoint="/posts" revalidate={120} />
</Suspense>
<Suspense fallback={<Skeleton label="Comments" />}>
<DataCard title="Comments" endpoint="/comments" />
</Suspense>
<Suspense fallback={<Skeleton label="Todos" />}>
<DataCard title="Todos" endpoint="/todos" revalidate={30} />
</Suspense>
</div>
</div>
)
}
async function DataCard({ title, endpoint, revalidate }: {
title: string
endpoint: string
revalidate?: number
}) {
const data = await getData<any[]>(endpoint, revalidate
? { next: { revalidate } }
: { cache: 'no-store' }
)
return (
<div style={{ border: '1px solid #ddd', borderRadius: 8, padding: 16 }}>
<h2>{title} <span style={{ fontSize: 14, color: '#666' }}>({data.length})</span></h2>
<ul>{data.slice(0, 5).map((item: any) => (
<li key={item.id}>{item.title ?? item.name ?? item.email}</li>
))}</ul>
</div>
)
}
function Skeleton({ label }: { label: string }) {
return <div style={{ border: '1px solid #eee', borderRadius: 8, padding: 16, opacity: 0.5 }}>
Loading {label}...
</div>
}
// app/dashboard-optimized/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function refreshSection(tag: string) {
revalidateTag(tag)
return { success: true }
}
❓ 常见问题
npm run dev)下,force-cache 仍会每次请求(方便调试)。生产模式(next start 或构建后)才会真正缓存。这是 Next.js 的设计决定——开发阶段始终获取最新数据。cache 或 next.revalidate 设置会被视为不同缓存条目。Promise.allSettled 或 try-catch 包装每个 fetch。常见模式:const results = await Promise.all(urls.map(u => fetch(u).catch(() => null)))。const ctrl = new AbortController(); setTimeout(() => ctrl.abort(), 5000); fetch(url, { signal: ctrl.signal })。推荐在 app 层封装统一的 fetch 客户端。📖 小节
- RSC 中的 fetch 默认使用
force-cache,相同 URL 自动缓存 cache: 'no-store'禁用缓存,适合实时数据next: { revalidate: N }实现时间窗口缓存(类似 ISR)next: { tags: [...] }+revalidateTag()实现按需缓存刷新Promise.all()并行请求避免瀑布流,可减少 60-80% 加载时间- Suspense 边界拆分实现流式加载,不需要等待所有数据就绪
revalidatePath()按路径清除页面级缓存
📝 作业
-
基础题(⭐):创建一个
app/time-demo/page.tsx,使用cache: 'no-store'和cache: 'force-cache'分别请求 World Time API,对比两个时间戳的差异,验证缓存行为。 -
进阶题(⭐⭐):构建一个
app/parallel-demo/page.tsx,使用Promise.all并行获取/users、/posts、/comments三个端点(使用 JSONPlaceholder API),将每个数据渲染到独立的<Suspense>边界中,展示流式加载效果。 -
挑战题(⭐⭐⭐):创建一个支持 CRUD 的任务列表页面:
app/tasks/page.tsx(显示任务列表,使用 tags 缓存)和app/tasks/actions.ts(添加/删除任务后调用revalidateTag('tasks')刷新列表)。添加乐观更新体验,确保写入后立即刷新。