cal.diy 性能实践:用 better-all 做依赖感知的异步并行,消除数据获取瀑布
在 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)
这里 fetchUser 与 fetchConfig 相互独立,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,再对 configPromise 和 fetchData(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)
}
})
逐点解析其语义:
- 任务以对象方法声明:
all()接收一个任务对象,键名(user、config、profile)成为最终返回对象的结果键,天然避免了Promise.all数组解构"顺序即契约"的脆弱性——新增/调换任务不必重新记忆数组下标。 - 依赖通过
this.$.任务名表达:this.$.user是 user 任务结果对应的 Promise。在profile任务内写await this.$.user,即声明"profile 依赖 user"。 - 调度器自动求最早启动时刻:这是该库的核心价值——文档原文表述为 "It automatically starts each task at the earliest possible moment"(它自动在最早可行时刻启动每个任务)。
user与config无任何依赖声明,会在同一微任务时机同时启动;profile在userresolve 后立即启动,哪怕config尚未返回。时间线上,profile的总耗时不再包含config的剩余耗时,关键路径从max(user, config) + profile缩短为user + profile(config 只要不晚于关键路径完成即可)。 - 一次性拿到全部结果:
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.md 与 AGENTS.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"消除,而非并行化掩盖。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00