首页
/ Cal.com(cal.diy)Server Components 并行数据获取:用组件组合消除 RSC 服务端瀑布请求

Cal.com(cal.diy)Server Components 并行数据获取:用组件组合消除 RSC 服务端瀑布请求

2026-09-07 18:02:10作者:苗圣禹Peter

本篇技术指南聚焦于 cal.diy(Cal.com 开源排期系统)仓库中收录的一条 Vercel React 最佳实践规则——"通过组件组合实现并行数据获取"(Parallel Data Fetching with Component Composition)。它解释了 React Server Components(RSC)在组件树内顺序执行 await 所造成的服务端瀑布延迟问题,并给出两种可复制的重构手法:将取数下沉到各子组件、以及利用 Layout + children 插槽解耦取数时机。读完后,你将掌握在 Next.js App Router 页面中识别、诊断并消除 RSC 服务端瀑布请求的完整方法,并能对照仓库中的真实路由代码验证相关并行化模式。

一、规则定位:一条 CRITICAL 级别的 RSC 服务端优化规则

该规则的完整原文位于 server-parallel-fetching.md,属于 vercel-react-best-practices 技能 中 45 条规则、8 个类别的一部分。从规则文件的 frontmatter 可以看到它的元信息:

  • title:Parallel Data Fetching with Component Composition
  • impactCRITICAL
  • impactDescriptioneliminates server-side waterfalls(消除服务端瀑布请求)
  • tagsserver, rsc, parallel-fetching, composition

SKILL.md 中的优先级表,server- 前缀规则整体归入第 3 优先级"Server-Side Performance"(HIGH),而"消除瀑布(Eliminating Waterfalls)"是整个体系中标注为 CRITICAL 的第一优先级。这条规则本身把 impact 标为 CRITICAL,说明它在服务端性能维度中被视为收益最大的一类改动——因为服务端串行 await 每叠加一层,页面首字节时间就完整多出一次网络/数据库往返。

同一内容也被收录进技能的完整编译文档 第 3.3 节,两个来源互相印证,可作为引用锚点。

二、问题本质:RSC 在组件树内是顺序执行的

规则开篇给出核心结论:"React Server Components execute sequentially within a tree."(React Server Components 在组件树内部是顺序执行的)。要理解这意味着什么,先看规则给出的反例:

错误写法(Sidebar 必须等 Page 的 fetch 完成才开始取数):

export default async function Page() {
  const header = await fetchHeader()
  return (
    <div>
      <div>{header}</div>
      <Sidebar />
    </div>
  )
}

async function Sidebar() {
  const items = await fetchSidebarItems()
  return <nav>{items.map(renderItem)}</nav>
}

这里的执行时序是:

  1. Page 是 async 组件,运行到 await fetchHeader() 时挂起;
  2. 只有 fetchHeader() resolve 之后,Page 才会继续执行到 return,此时 <Sidebar /> 这个组件才首次被创建
  3. <Sidebar /> 被创建后才轮到 fetchSidebarItems() 发起。

于是两个本无依赖关系的请求被串成了 header → sidebar 的瀑布:总耗时约等于两次取数延迟之和,而不是两者中的最大值。关键在于——在 RSC 中,组件是"函数体执行时才取数",而子组件的"开始执行"时点被父组件的 return 位置锁死了。父组件 return 之前的一切 await,都会推迟其后所有子树的取数启动时间。

三、正确写法一:将取数下沉到各子组件,用组合实现并行

规则给出的第一种正解是把 fetchHeader()Page 里搬出去,让每个"要数据的组件"自己负责取数:

正确写法(Header 与 Sidebar 同时取数):

async function Header() {
  const data = await fetchHeader()
  return <div>{data}</div>
}

async function Sidebar() {
  const items = await fetchSidebarItems()
  return <nav>{items.map(renderItem)}</nav>
}

export default function Page() {
  return (
    <div>
      <Header />
      <Sidebar />
    </div>
  )
}

重构后 Page 变成同步组件:它的函数体没有任何 await,可以立即返回 <Header /><Sidebar />。两个 async 子组件被 React 依次渲染,而它们各自的 await独立并发的——fetchHeader()fetchSidebarItems() 几乎同时发出,总耗时收敛到两者中较慢的那个。

这一手之所以成立,是因为组件组合改变了"取数启动时点":父组件不再替子组件挡在前面取数,子树越独立,并行窗口就越大。它也与同系列规则 async-parallel.md 中"无依赖的异步操作用 Promise.all() 并发执行"的思想一脉相承——区别在于,当各段数据分别被不同 UI 区块消费时,用组件边界来表达并行比在一个函数里手写 Promise.all 更自然,还能让每个区块独立流式、独立报错。

仓库源码中可以看到这种"并发取数、统一 await"模式在 API 路由里的实际应用。例如预订提醒定时任务 bookingReminder/route.ts 中,对每个 booking 的参会者翻译数据就是先批量启动 Promise,再一次性等待:

const attendeesListPromises = booking.attendees.map(async (attendee) => {
  return {
    name: attendee.name,
    email: attendee.email,
    timeZone: attendee.timeZone,
    language: {
      translate: await getTranslation(attendee.locale ?? "en", "common"),
      locale: attendee.locale ?? "en",
    },
  };
});

const attendeesList = await Promise.all(attendeesListPromises);

从源码结构看,这里是同一条规则在"函数内"场景的落法:先创建全部 Promise(全部立即开始),再统一 await Promise.all(...),而不是在循环里逐个 await。组件组合式并行是它在"组件树内"场景的对应物。

四、正确写法二:用 Layout 的 children 插槽解耦取数时机

并非所有页面都能轻易把 Page 拆成纯布局。规则给出了第二种等价手法——Layout + children prop

async function Layout({ children }: { children: ReactNode }) {
  const header = await fetchHeader()
  return (
    <div>
      <div>{header}</div>
      {children}
    </div>
  )
}

async function Sidebar() {
  const items = await fetchSidebarItems()
  return <nav>{items.map(renderItem)}</nav>
}

export default function Page() {
  return (
    <Layout>
      <Sidebar />
    </Layout>
  )
}

这种写法里 Layout 自己仍然要 await fetchHeader(),看起来似乎又回到了"父组件先取数"。但从 JSX 求值顺序看,<Sidebar /> 这个子元素是在 Page 的 return 语句里先于 Layout 函数体执行就被创建出来的,React 渲染 Layout 的 children 时会同步驱动 Sidebar 的执行;于是 fetchHeader()(Layout 内)与 fetchSidebarItems()(Sidebar 内)依然是并发发出的。换言之,children 插槽把"页面主体"从 Layout 的 await 之后解放了出来,Layout 只负责自己的框架数据,主体数据由子树自己并行拉取。

两种正解的选择建议:

  • 页面结构允许时,优先用"取数下沉 + 组合"(写法一),职责最清晰;
  • 框架级取数(如全局 header、权限信息)天然属于 Layout 时,用写法二,让 Layout 与内容子树各取所需、互不阻塞。

五、配套手法:与 Suspense、cache 组合使用

组件组合解决的是"并行启动",但要让并行收益真正体现在首屏时间上,通常还需搭配同系列的两条规则:

  1. 策略性 Suspense 边界async-suspense-boundaries.md):<Header /><Sidebar /> 虽然并发取数,但如果其中某一个特别慢,整棵子树要等最慢的那个才提交。用 <Suspense> 分别包裹两个区块后,快的区块先流式上屏,慢的区块以 fallback 占位。仓库中即可见类似用法,如 troubleshoot/layout.tsx/availability/troubleshoot/layout.tsx) 第 13 行用 <Suspense fallback={<LoaderIcon />}>{children}</Suspense> 包裹页面主体。规则文档同时明确了 Suspense 不适用的场景(布局决策所需的关键数据、首屏 SEO 关键内容、很小的快查询等),取舍点是"更快的首帧绘制 vs 可能的布局偏移"。
  2. React.cache() 请求内去重server-cache-react.md):组合化后可能出现多个组件消费同一份数据(例如 Header 与 Sidebar 都需要当前用户)。用 cache(async () => ...) 包裹取数函数后,同一请求内的多次调用只执行一次查询,避免"拆得越细、重复查询越多"的反效果。

此外还要注意配套的 server-serialization.md 规则:并行取数拿到的数据若还要传给客户端组件,只有客户端真正用到的字段才应跨过 RSC 边界,序列化体积会直接影响页面重量。

六、实操检查清单

在 cal.diy 这类大型 Next.js 应用中应用该规则,可按以下步骤自查:

  1. 找出页面入口里的顶层 await:检查各路由 page.tsx 的默认导出组件,若函数体内先 await fetchX() 再渲染子组件,且 X 的数据与子树数据无依赖,即为潜在瀑布;
  2. 判断依赖关系:子树数据是否依赖父级 await 的结果?若有依赖(例如子树 ID 来自父级查询),应参照 async-parallel.md 与 AGENTS.md 1.2 节的依赖链并行思路重排,而不是盲目拆组件;
  3. 拆分为取数型子组件:把 await 下沉到各数据区块组件内部(写法一),或在框架组件中用 children 插槽隔离(写法二);
  4. 叠加 Suspense 与 cache:为慢区块加独立 Suspense 边界,为共享数据加 cache()
  5. 验证:本地起服务后用 Network/DevTools 观察该页面是否发出并发的后端请求,服务端日志中两个取数请求的发起时间应接近同一时刻,而非首尾相接。

七、参考路径汇总

内容 相对路径
本文对应的规则原文 server-parallel-fetching.md
规则集总览与优先级表 SKILL.md
编译版完整指南(3.3 节同文) AGENTS.md
关联规则:Promise.all 并发 async-parallel.md
关联规则:Suspense 边界 async-suspense-boundaries.md
关联规则:React.cache 去重 server-cache-react.md
仓库内并行取数实例 bookingReminder/route.ts
仓库内 Suspense 实例 troubleshoot/layout.tsx/availability/troubleshoot/layout.tsx)

一句话总结:在 RSC 中,await 写在哪里,取数就从哪里才开始。把取数从"父级先取再传"改写为"各子组件自取",或借 children 插槽让子树先于 Layout 的 await 启动,是消除服务端瀑布、让总耗时从"各请求之和"降为"最慢请求"的最小且收益最大的重构。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391