首页
/ cal.diy 性能实践:用 better-all 做依赖感知的异步并行,消除数据获取瀑布

cal.diy 性能实践:用 better-all 做依赖感知的异步并行,消除数据获取瀑布

2026-09-05 10:40:27作者:傅爽业Veleda

在 React/Next.js 服务端取数场景中,"先等 A,再等 B"的瀑布式 await 是延迟放大的常见来源。cal.diy(开源排期基础设施项目)在其 Agent 技能库中收录了 Vercel React 最佳实践的第一优先级规则——基于依赖的并行化(Dependency-Based Parallelization):当多个异步操作之间存在部分依赖关系时,使用 better-all 库让每个任务在"最早可行时刻"自动启动,规则自评影响等级为 CRITICAL(2-10 倍改进空间)。读完本文,你将理解 Promise.all 在部分依赖场景下的局限、better-all 的声明式依赖 API 写法,以及如何在这套规则体系中正确选择并行策略。

规则定位:cal.diy 中"消除瀑布"类别的最高优先级条目

该规则文档位于 async-dependencies.md,同时收录在 Agent 技能目录 agents/skills/vercel-react-best-practices/rules/async-dependencies.md 下。根据技能总览 SKILL.md 的分类,整套指南共 45 条规则、8 个类别,按影响程度排优先级,其中:

优先级 类别 影响等级 规则前缀
1 Eliminating Waterfalls(消除瀑布) CRITICAL async-
2 Bundle Size Optimization CRITICAL bundle-
3 Server-Side Performance HIGH server-

async-dependencies 属于优先级 1 的 async- 前缀家族,与以下兄弟规则配合构成完整的"消除瀑布"方法论:

  • async-parallel.md:无依赖操作直接用 Promise.all() 并发;
  • async-api-routes.md:API 路由中"尽早启动 Promise、尽量晚地 await",并明确指路"依赖链更复杂时使用 better-all";
  • async-defer-await.md:把 await 推迟到真正使用的分支内,避免阻塞用不到结果的代码路径。

完整的编译版指南见 AGENTS.md

问题:Promise.all 解决不了"部分依赖"

规则文档给出的反例是一段非常典型的取数代码:

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

这里 fetchUserfetchConfig 相互独立,Promise.all 确实让二者并发了;但 profile 依赖 user.id,只能等前一批全部完成后再发起。时间线上,fetchConfig 哪怕第 50ms 就返回了,fetchProfile 也只能在 fetchUser 完成后才开始——无依赖的 config 结果被白白地压在关键路径之外,profile 的启动被不必要地推迟。这就是文档所指出的 "profile waits for config unnecessarily"(profile 无谓地等待 config)。

async-api-routes 规则中的反例呈现了同构问题:

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 })
}

三次 await 完全串行,config 等待 auth、data 又等待二者,形成完整瀑布。该规则的修正方案是"先启动、后等待"——auth()fetchConfig() 立即发起、先 await session,再对 configPromisefetchData(session.user.id)Promise.all。这种手写方案适合依赖关系固定且简单的场景;一旦依赖边增多(如 profile 还依赖 config 里的某个开关),手写 Promise 的组合与 await 顺序会迅速变得难以审查,这正是 better-all 的用武之地。

方案:better-all 的声明式依赖 API

规则文档给出的正例如下:

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)
  }
})

逐点解析其语义:

  1. 任务以对象方法声明all() 接收一个任务对象,键名(userconfigprofile)成为最终返回对象的结果键,天然避免了 Promise.all 数组解构"顺序即契约"的脆弱性——新增/调换任务不必重新记忆数组下标。
  2. 依赖通过 this.$.任务名 表达this.$.user 是 user 任务结果对应的 Promise。在 profile 任务内写 await this.$.user,即声明"profile 依赖 user"。
  3. 调度器自动求最早启动时刻:这是该库的核心价值——文档原文表述为 "It automatically starts each task at the earliest possible moment"(它自动在最早可行时刻启动每个任务)。userconfig 无任何依赖声明,会在同一微任务时机同时启动;profileuser resolve 后立即启动,哪怕 config 尚未返回。时间线上,profile 的总耗时不再包含 config 的剩余耗时,关键路径从 max(user, config) + profile 缩短为 user + profile(config 只要不晚于关键路径完成即可)。
  4. 一次性拿到全部结果await all(...) 返回的普通对象同时包含三个任务的结果,后续解构使用,无需再组织多组 await。

需要注意的适用前提:better-all 解决的是"任务间存在部分依赖"的问题。若任务完全独立,直接用 Promise.all 更轻量(见 async-parallel.md);若某个分支根本不需要某次请求,优先考虑 async-defer-await 的"推迟到分支内再 await",从源头省掉这次开销,而不是靠并行把它藏起来。

cal.diy 仓库中的落地现状

基于当前仓库的实际检索结果(yarn.lock 与各 package.json 均未出现 better-all 依赖声明,源码中也无 import { all } from 'better-all' 的直接使用),可以确认:该库目前以"编码规则/重构指南"的形式存在于 Agent 技能库中,是 cal.diy 团队为自动化代码生成与人工评审预设的性能基线,而非已引入的运行时依赖。这一组织方式本身值得参考:

  • 规则文档采用统一的 frontmatter 结构(title / impact / impactDescription / tags),使"影响等级"可被 Agent 工具程序化消费,在 CRITICAL 级规则命中时优先处理;
  • 每条规则都包含"错误示例 + 正确示例 + 参考"三段式,SKILL.md 明确了触发时机——编写新组件、实现数据获取、性能评审与重构时应当对照这些规则;
  • async-api-routes.mdAGENTS.md 中均交叉引用了本规则,形成"简单依赖用早启动 + Promise.all,复杂依赖用 better-all"的完整决策链。

因此在 cal.diy 中应用该规则的实际路径是:当你在 API 路由、Server Actions 或 tRPC 处理器里发现"取数 A/B 独立、取数 C 依赖 A(或 A+B)"的形态时,先按 async-api-routes 的方式尽早启动 Promise;当依赖边达到 3 条以上、手写 Promise 组合开始难以维护时,引入 better-all 按上文声明式写法重构。若引入该库,它作为普通 npm 依赖安装即可,不改变 Next.js 的应用结构。

选择策略速查

场景 推荐策略 依据规则
操作互相独立 Promise.all([...]) async-parallel.md
独立操作 + 一条简单依赖链(1 个下游) 尽早启动 Promise,晚点 await,必要时 Promise.all 收尾 async-api-routes.md
多条/网状部分依赖(任务互相引用结果) better-all 声明式任务对象 + this.$.任务名 async-dependencies.md
依赖结果只被部分分支使用 await 移入分支内,跳过无用请求 async-defer-await.md

关键要点回顾

  • 问题本质Promise.all 只并发"同一批次"的任务,批次间的依赖会让无关节点的剩余耗时泄漏进关键路径(config 的剩余耗时拖慢了 profile 的启动)。
  • better-all 的机制:声明式任务对象 + this.$.任务名 依赖引用,调度器保证每个任务在"依赖就绪的最早时刻"启动,规则自评带来 2-10 倍的改进空间(这是技能文档的自评级别,实际收益取决于各任务耗时结构)。
  • 在 cal.diy 中的形态:该规则以 CRITICAL 级编码基线存在于 agents/skills/vercel-react-best-practices 技能库中,指导 API 路由与 Server Actions 的重构;仓库当前未将其声明为依赖,引入与否取决于具体代码中部分依赖链的复杂度。
  • 不要过度套用:完全独立的任务无需引入该库;可被分支跳过的请求应优先用"推迟 await"消除,而非并行化掩盖。
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
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
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384