首页
/ Cal.com 性能规则解析:async-api-routes——消除 API Routes 与 Server Actions 中的串行等待瀑布

Cal.com 性能规则解析:async-api-routes——消除 API Routes 与 Server Actions 中的串行等待瀑布

2026-09-05 10:03:23作者:冯梦姬Eddie

本文基于 Cal.com 仓库内置的 Vercel React 性能规则库,深入讲解 async-api-routes 规则(Prevent Waterfall Chains in API Routes):为什么 API 路由中逐个 await 会形成"请求瀑布",如何通过"提前发起、延后等待"重排 Promise 来并行化独立操作,以及面对部分依赖的复杂调用链时如何借助 better-all 进一步最大化并行度。读完后你可以直接在 Next.js API Routes、Server Actions 乃至任意异步服务端代码中套用这套模式,并对照本仓库中的规则原文与真实路由代码验证效果。

规则定位与元数据

async-api-routes 是仓库中随 Agent 技能库分发的一条性能规则文件,其完整路径为 async-api-routes.md。文件头部 frontmatter 声明了这条规则的元数据,值得先完整看一遍:

---
title: Prevent Waterfall Chains in API Routes
impact: CRITICAL
impactDescription: 2-10× improvement
tags: api-routes, server-actions, waterfalls, parallelization
---
  • title:规则主题——防止 API 路由中出现瀑布式(Waterfall)串行链;
  • impact: CRITICAL:在 SKILL.md 定义的 8 个优先级类别中,"Eliminating Waterfalls"(消除瀑布)是优先级第 1、影响级别 CRITICAL 的类别,async- 前缀正是该类别的命名前缀。也就是说,这条规则属于整个规则库中优先级最高的一档;
  • impactDescription: 2-10× improvement:这是规则文档自身给出的收益评级(指瀑布链越长,并行化后相对串行总耗时的提升倍数越大),并非实测基准数据,引用时需注意这是文档标注而非仓库测量结果;
  • tagsapi-routes, server-actions, waterfalls, parallelization,明确了适用范围(API Routes 与 Server Actions)与手段(并行化)。

同一规则文件在仓库中以两份完全相同的内容存放(已验证逐字节一致):一份在 .opencode/skill/vercel-react-best-practices/rules/ 供 OpenCode 类 Agent 消费,另一份在 agents/skills/vercel-react-best-practices/rules/ 供其他 Agent 框架消费,二者共享 AGENTS.md 这份将 45 条规则全部展开的编译版文档。

核心原理:提前发起 Promise,延后 await

规则正文的第一句话就是全部要点:

In API routes and Server Actions, start independent operations immediately, even if you don't await them yet. (在 API 路由和 Server Actions 中,独立的操作应当立即开始,即使你此刻还没有 await 它们。)

这里利用的是 JavaScript 异步模型的语义:调用一个返回 Promise 的函数时,底层工作(网络请求、数据库查询)在函数被调用的那一刻就启动了;await 只是挂起当前执行流直到结果就绪,并不会"推迟"请求的发起。因此,只要先调用、后等待,多个独立请求的耗时就可以自然重叠:

  • 串行 await 的总耗时 ≈ 各操作耗时之和(Σ tᵢ);
  • 并行发起后的总耗时 ≈ 依赖链上的关键路径耗时(max 或分段串行)。

瀑布链越长、单次网络/IO 延迟越高,差距越明显——这正是该规则被标注为 2-10× 改进空间的原因(规则原文的口径)。

反例:config 等待 auth,data 再等两者

规则给出的错误示例是一段典型的三段串行 GET 处理函数(规则文件第 14-21 行):

export async function GET(request: Request) {
  const session = await auth()
  const config = await fetchConfig()
  const data = await fetchData(session.user.id)
  return Response.json({ data, config })
}

问题在于依赖关系被高估了:

  1. fetchConfig() 只读配置,与 auth() 结果毫无关系,却被排在其后,白白等了一个 auth() 的完整延迟;
  2. fetchData(session.user.id) 确实依赖 session,这一点无法优化;
  3. 于是总耗时是 t(auth) + t(config) + t(data),而其中 config 本可与前两者重叠。

规则用一句话概括了这个反例的病灶:config waits for auth, data waits for both(config 在等 auth,data 在等前两者)。

正例:auth 与 config 立即发起,Promise.all 收口

对应的正确写法(规则文件第 26-36 行):

export async function GET(request: Request) {
  const sessionPromise = auth()
  const configPromise = fetchConfig()
  const session = await sessionPromise
  const [config, data] = await Promise.all([
    configPromise,
    fetchData(session.user.id)
  ])
  return Response.json({ data, config })
}

逐行拆解其结构:

  • 第 1-2 行(提前发起)auth()fetchConfig() 两个函数在被调用的瞬间就各自开始网络往返,此时只是把 Promise 对象存入变量,不做任何等待;
  • 第 3 行(依赖点才 await)fetchData 需要 session.user.id,这里才第一次 await sessionPromise。由于 config 已经在途,这一次等待并不阻塞 config 的完成;
  • 第 4-7 行(收口)Promise.all([configPromise, fetchData(session.user.id)]) 把"已在途的 config"和"刚启动的 data"一起等待。configPromise 大概率早已 resolve,实际等待时间由 fetchData 主导;
  • 最终总耗时t(auth 与 config 中的较慢者) + t(data),config 的耗时被完全"藏"进了 auth/data 的窗口内。

可以抽象成三步心智模型,适用于任何 API Route / Server Action / 异步服务端函数:

  1. 列出所有操作的真实依赖(谁的结果被谁消费);
  2. 无依赖的操作在函数开头立即调用,保存 Promise;
  3. 只在真正需要结果的"依赖点"await,最终用 Promise.all 收口,避免"顺手 await"。

复杂依赖链:用 better-all 自动最大化并行

规则原文的最后一句给出了进阶指引(第 38 行):

For operations with more complex dependency chains, use better-all to automatically maximize parallelism (see Dependency-Based Parallelization).

即当操作之间存在部分依赖(A 独立、B 依赖 A、C 也依赖 A……)时,手动管理"谁该提前发起"容易出错,仓库中配套的 async-dependencies.md 规则给出了 better-allall() 辅助函数方案。它接收一个由命名异步任务组成的对象,每个任务内部可以通过 this.$.<任务名> 引用其他任务(返回 Promise,自动等待其结果),调度器会自动让每个任务在最早可能时刻启动:

反例(profile 不必要地等待 config,因为 config 与 user 串行在同一个 Promise.all 之前看似并行,但 profile 被推迟到两者都完成后才开始):

const [user, config] = await Promise.all([
  fetchUser(),
  fetchConfig()
])
const profile = await fetchProfile(user.id)

正例(config 与 profile 并行执行,profile 只等 user):

import { all } from 'better-all'

const { user, config, profile } = await all({
  async user() { return fetchUser() },
  async config() { return fetchConfig() },
  async profile() {
    return fetchProfile((await this.$.user).id)
  }
})

对比 async-api-routes 的手动写法,better-all 的价值在于:依赖关系以声明方式表达在任务体内,并行度的最大化由库保证,开发者不再需要心算"哪些 Promise 应该提前挂起"。

在 Cal.com 仓库中的落地参照

结合本仓库源码结构,可以说明这条规则的实际适用面:

  • Next.js API Routes:Cal.com 前端应用 apps/web 下的路由采用标准 App Router API Route 形态,例如 csrf/route.ts 中直接导出 export async function GET(req: Request)。该路由目前只有单一线性流程(生成 token 并设置 Cookie),无并行化需求;但它展示了本仓库 API Route 的标准写法——任何在其中新增多个独立 IO 的场景(如同时校验会话、查询用户配置、拉取第三方状态),都应按"提前发起、延后等待"重排;同目录下的 ip/route.tsvideo/recording/route.ts 也是同类形态的处理函数;
  • Server Actions 与 tRPC 处理器apps/web 中大量数据操作经由 tRPC 路由(packages/trpc/server/)与服务端组件完成。规则 tags 中明确包含 server-actions,因为 Server Action 内部的异步代码与 API Route 遵循同样的事件循环语义,瀑布问题同源、解法同构;
  • NestJS API v2apps/api/v2 中的 NestJS 控制器方法同样是 async 函数,其中对独立 Repository 查询的连续 await 同样构成瀑布链。从源码结构看,该规则文件由规则库统一分发,其适用对象不限于 Next.js,而是覆盖所有"在单个 async 函数中编排多个 IO"的服务端代码。

工程细节与注意事项

套用该模式时,以下几点属于从异步语义推导出的工程判断(规则原文未展开,属补充建议):

  1. 错误传播语义变化:手动提前发起后,Promise.all 中任一 Promise reject 会拒绝整个集合,且先于其失败的兄弟任务不会自动取消(网络请求已发出)。若需要"部分失败可用",可改用 Promise.allSettled 或对独立 Promise 单独 catch
  2. 副作用操作要谨慎提前:纯读取(auth、fetchConfig)提前发起是安全的;但若操作带有写入或扣减类副作用,提前开始会改变"前置校验失败则不执行"的语义,这类操作应保持串行;
  3. 资源与限流:并行发起意味着瞬时并发请求数上升,依赖服务有配额限制时需要考虑连接池与限流策略;
  4. 不要为了并行而并行:规则的前提是"独立操作"。若 B 的入参来自 A 的结果,B 就必须等到 A——强行提前只能传一个尚未 ready 的值,属于逻辑错误。判断依据始终是真实数据依赖,而非代码书写顺序。

检查清单

评审或编写 API Route / Server Action 时,可按以下问题自检(由规则正文直接推得):

  • 函数开头是否存在"先 await 操作 1、再 await 操作 2"的连续语句,而操作 2 并不消费操作 1 的结果?若是,把调用提前、await 推迟;
  • 是否存在多个"相互独立、仅各自依赖不同上游"的操作被写成串行?用 Promise.all 收口;
  • 是否存在部分依赖的复杂调用链?考虑引入 better-allall()(参见 async-dependencies.md);
  • 每个 await 位置是否都对应一个真实的数据依赖点("await late, only where needed")?

综上,async-api-routes 规则虽然只有一页,但它给出的"start early, await late"是服务端异步编排中最基础也收益最高的一条纪律:先画依赖、再定发起顺序、最后在依赖点收口,即可把多数 API Route 中的串行延迟压缩到关键路径长度。

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

项目优选

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