- 读者画像:使用 Next.js 13+/14 的开发者,想吃透 Server Components、缓存与并发。
- 技术关键词:Server Components、Request Memoization、Full-route Cache、Revalidation、React Suspense、用 use 读取 promise。
一、时代变了:数据获取从"接口即拉"到"服务端即思考"
在 React 传统客户端时代,数据获取是"组件挂载 → useEffect → fetch → setState"。Next.js 13+ 以后,服务端组件成为主角,数据获取"前置"到了服务器渲染阶段,顺便把缓存、并发、流式、SEO 一锅端正。
新三剑客各司其职:
- fetch:带有框架级缓存语义的"带脑子"的请求器。
- cache:对任意纯函数做"结果记忆",并与 React 的数据获取黏合。
- use:对 promise 说"请你等会儿",自动挂起,搭配 Suspense 实现流式与并发。
小图标提示:🧠 表示"有状态的聪明操作",⚙️ 表示"底层机制",🚀 表示"性能与并发"。
二、fetch 的真身:不仅是请求,更是声明缓存策略的"合同"
在 Next.js App Router 中,fetch 默认不是裸奔:
- 同一路由请求内:同 URL 和相同选项的 fetch 会"请求记忆"(Request Memoization),第二次读取直接复用上次结果(🧠 节流降噪)。
- 全路由层面:配合缓存策略,结果可被渲染层缓存(Full-route Cache),走静态化或增量再生。
核心选项:
- cache: 'force-cache'(默认)、'no-store'
- next: { revalidate: 秒数 } 增量再验证
- next: { tags: ['...'] } 用于按 tag 失效
- headers、method 等则决定"请求唯一性"。
最常见的三种姿势:
javascript
// 1) 完全静态,可被构建或首请求缓存
export async function getPostStatic(id) {
const res = await fetch(`https://api.example.com/posts/${id}`, {
// 默认就是 force-cache
// cache: 'force-cache'
});
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
}
// 2) 纯动态,永远直连后端
export async function getPostDynamic(id) {
const res = await fetch(`https://api.example.com/posts/${id}`, {
cache: 'no-store',
});
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
}
// 3) ISR(增量再验证):N 秒后下一次请求触发再拉取
export async function getPostISR(id) {
const res = await fetch(`https://api.example.com/posts/${id}`, {
next: { revalidate: 60 }, // 60 秒
});
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
}
注意点(⚙️ 底层脾性):
- 请求唯一性键由 URL + method + headers + body + fetch 选项共同决定。
- 同一请求周期内,请求记忆会避免重复 IO,避免"你问一次我答两次"的尴尬。
- 使用 cookies/session 影响静态化,Next 会将页面判定为动态(例如读取 request headers)。
三、cache:给纯函数一把"记忆锤",把数据源变成幂等接口
cache(fn) 返回一个"会记住输入→输出"的函数,缓存键由参数值决定。与 fetch 不同,它是对"函数级"层面的记忆,常用于聚合数据、组合查询、或昂贵计算。
- 纯函数?好朋友。无副作用,输入相同,输出稳定(🧠 可安全记忆)。
- 动态参数?仍可记忆,但注意参数可序列化和稳定性。
- 与 revalidate/tag 搭配?可以,但需要配合 route handler 的 revalidateTag 等手段去主动失效。
例:聚合用户详情与文章列表,避免重复计算。
javascript
import { cache } from 'react';
// 带记忆的聚合查询
export const getUserBundle = cache(async function getUserBundle(userId) {
const [userRes, postsRes] = await Promise.all([
fetch(`https://api.example.com/users/${userId}`, { next: { revalidate: 300 } }),
fetch(`https://api.example.com/users/${userId}/posts`, { next: { revalidate: 300 } })
]);
if (!userRes.ok || !postsRes.ok) throw new Error('Upstream error');
const [user, posts] = await Promise.all([userRes.json(), postsRes.json()]);
return { user, postsCount: posts.length };
});
// 相同 userId 多处调用,仅首次会命中 IO,后续来自函数级缓存
注意点(⚙️):
- cache 作用域在 server runtime 内,与请求记忆不同,生命周期可跨请求,但实际寿命取决于部署与进程回收(在 Serverless 场景不保证长寿,像金鱼的记忆力,有时说走就走)。
- 如果外部数据变化频繁,记得设计失效策略:例如使用 revalidateTag 并在数据变更后打标签失效。
四、use:让 promise 说人话------"我还没好,你先挂起"
React 的 use(来自 react 包,不是 React Hook)可在 Server Components 或 RSC 环境中"读取 promise"。当 promise 未完成时,组件会挂起(suspend),Suspense 辅助显示 fallback,实现并发与流式。
- use(promise) 返回已解析的值,若未解析则触发挂起。
- 在 Server Components 中,挂起会被流式分块传输,Out-of-order chunking 提升首屏时间。
- 在 Client Components 中使用 use 需要 React 19+ 并受限,通常通过 Server Components 来承担数据获取更自然。
例:并发拉取与 use 协同:
javascript
// app/users/[id]/page.js
import { Suspense } from 'react';
import { use } from 'react';
function UserView({ userPromise }) {
const user = use(userPromise); // 如果还没来,就挂起
return (
<section>
<h2>👤 {user.name}</h2>
<p>Email: {user.email}</p>
</section>
);
}
function PostsView({ postsPromise }) {
const posts = use(postsPromise);
return (
<section>
<h3>📝 Posts</h3>
<ul>
{posts.map(p => <li key={p.id}>{p.title}</li>)}
</ul>
</section>
);
}
export default function Page({ params }) {
const { id } = params;
// 并发启动请求:避免串行瀑布
const userPromise = fetch(`https://api.example.com/users/${id}`, { next: { revalidate: 120 } }).then(r => r.json());
const postsPromise = fetch(`https://api.example.com/users/${id}/posts`, { cache: 'no-store' }).then(r => r.json());
return (
<>
<h1>🚀 用户信息</h1>
<Suspense fallback={<p>加载用户...</p>}>
<UserView userPromise={userPromise} />
</Suspense>
<Suspense fallback={<p>加载文章列表...</p>}>
<PostsView postsPromise={postsPromise} />
</Suspense>
</>
);
}
亮点:
- 两个请求并发启动,组件内用 use 消费。
- 每个分块独立 Suspense,谁先好谁先渲染,用户先看到头像再看列表,首屏更快。
五、三剑客联手:从"拉数据"到"声明数据生命周期"
一套实战示范:产品页,需求如下
- 详情相对稳定:可 ISR。
- 库存变化频繁:要动态。
- 推荐列表可缓存 5 分钟。
- 用户点击"刷新推荐"后,手动失效。
目录:app/products/[id]/page.js
javascript
import { Suspense } from 'react';
import { cache } from 'react';
import { revalidateTag } from 'next/cache';
import { use } from 'react';
// 1) 详情:ISR 60 秒
const getProduct = cache(async (id) => {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: { revalidate: 60, tags: [`product:${id}`] }
});
if (!res.ok) throw new Error('Failed to fetch product');
return res.json();
});
// 2) 库存:纯动态
const getStock = cache(async (id) => {
const res = await fetch(`https://api.example.com/stock/${id}`, { cache: 'no-store' });
if (!res.ok) throw new Error('Failed to fetch stock');
return res.json();
});
// 3) 推荐:5 分钟缓存,可标签失效
const getRecommendations = cache(async (id) => {
const res = await fetch(`https://api.example.com/reco?pid=${id}`, {
next: { revalidate: 300, tags: [`reco:${id}`] }
});
if (!res.ok) throw new Error('Failed to fetch recommendations');
return res.json();
});
// 小小 action:允许用户触发标签失效(需要 Next.js 的 Server Actions)
export async function refreshRecommendations(formData) {
'use server';
const id = formData.get('id');
revalidateTag(`reco:${id}`);
}
function ProductSection({ productP }) {
const product = use(productP);
return (
<section>
<h2>📦 {product.title}</h2>
<p>{product.description}</p>
<p>价格:¥{product.price}</p>
</section>
);
}
function StockSection({ stockP }) {
const stock = use(stockP);
return (
<section>
<h3>📊 库存</h3>
<p>剩余:{stock.remaining}</p>
</section>
);
}
function RecoSection({ recoP, id }) {
const reco = use(recoP);
return (
<section>
<h3>✨ 为你推荐</h3>
<ul>
{reco.items.map(r => <li key={r.id}>{r.title}</li>)}
</ul>
<form action={refreshRecommendations}>
<input type="hidden" name="id" value={id} />
<button type="submit">🔄 刷新推荐</button>
</form>
</section>
);
}
export default function Page({ params }) {
const { id } = params;
// 并发触发 promise,搭配 use 消费
const productP = getProduct(id);
const stockP = getStock(id);
const recoP = getRecommendations(id);
return (
<>
<h1>🛒 商品详情</h1>
<Suspense fallback={<p>加载商品详情...</p>}>
<ProductSection productP={productP} />
</Suspense>
<Suspense fallback={<p>查询库存...</p>}>
<StockSection stockP={stockP} />
</Suspense>
<Suspense fallback={<p>计算推荐...</p>}>
<RecoSection recoP={recoP} id={id} />
</Suspense>
</>
);
}
这里,我们把生命周期"声明"出来:
- 详情:60 秒自动再生。
- 库存:永远直连,不缓存。
- 推荐:5 分钟缓存,可手动失效(标签模式)。
六、底层原理串讲:从字节、管线到缓存层
- 请求记忆(Request Memoization):同一请求周期内的相同 fetch 调用,框架在内存里存放 promise,复用其结果,避免重复触发 IO。像同车厢的人合并点餐。
- 渲染管线(React Flight + Suspense):Server Components 渲染成特殊 payload 流,客户端逐块接收并复用缓存树。use 遇到未完成 promise 就挂起,Suspense 提供局部 fallback,达成"水龙头先出清水,后出冰块"的体验。
- 缓存边界(Full-route Cache vs. Per-fetch):页面整体可被缓存为产物,也可在每个数据源级别设定 revalidate/标签。两者叠加时,以更"动态"的一方决定最终是否能静态化。
- 失效策略:时间(revalidate 秒数)是一把日晷,标签失效是一把按钮。按钮在你手里,别乱按。
七、易踩坑清单(带幽默警示牌)
- 混用请求头导致缓存键剧增:headers 里放入无用的随机值,就像给每份外卖改名字,快递员迷路了。
- 在 Client Component 里乱用 fetch:你以为在省油,其实在前端点火自燃。数据获取尽量交给 Server Components。
- cache 包装非纯函数:今天返回 42,明天返回"香蕉",记忆系统直接人格分裂。
- 忘了并发:顺序 await 导致"瀑布",用户在瀑布下冲了个冷水澡。
- 忘了错误边界:Suspense 负责等待,ErrorBoundary 负责兜底;别让用户只看到永恒的"加载中"。
八、性能调优小抄
- 能静态,尽量静态;能 ISR,就给个合理 revalidate。
- 高频变化的接口用 cache: 'no-store',避免陈旧。
- 并发拉取 + use 消费,搭配多个 Suspense,首屏更快。
- 用 tags 组织失效域:按资源 ID、按类型归类。
- 在 route handlers 内进行服务端合并请求,减少下游次数。
九、最小可运行样例:Next.js App Router
目录结构(简化):
- app/page.js 首页,拉三段数据并发显示
- app/actions.js Server Action 示例
app/page.js
javascript
import { Suspense } from 'react';
import { cache } from 'react';
import { use } from 'react';
const getA = cache(async () => {
const r = await fetch('https://api.example.com/a', { next: { revalidate: 30 } });
return r.json();
});
const getB = cache(async () => {
const r = await fetch('https://api.example.com/b', { cache: 'no-store' });
return r.json();
});
const getC = cache(async () => {
const r = await fetch('https://api.example.com/c', { next: { revalidate: 300, tags: ['c'] } });
return r.json();
});
function A({ p }) { const d = use(p); return <div>🅰️ {d.value}</div>; }
function B({ p }) { const d = use(p); return <div>🅱️ {d.value}</div>; }
function C({ p }) { const d = use(p); return <div>🆑 {d.value}</div>; }
export default function Page() {
const aP = getA();
const bP = getB();
const cP = getC();
return (
<main style={{ display: 'grid', gap: 12 }}>
<h1>🚦 三路并发流</h1>
<Suspense fallback={<p>加载 A...</p>}><A p={aP} /></Suspense>
<Suspense fallback={<p>加载 B...</p>}><B p={bP} /></Suspense>
<Suspense fallback={<p>加载 C...</p>}><C p={cP} /></Suspense>
</main>
);
}
十、收尾:写给未来的你
- 把数据当"素材",把缓存当"工艺",把 use 当"流水线控制器"。
- 能在 Server Components 里解决的事,不要放到客户端"加班"。
- 记忆不是目的,响应才是目的;缓存不是玄学,是一门工程学。