)
Mastra React 工程实践用 Promise.all 消除独立异步操作的水瀑效应Waterfall【免费下载链接】mastraMastra is the modern TypeScript framework for AI-powered applications and agents.项目地址: https://gitcode.com/GitHub_Trending/ma/mastra导读本文讲解 Mastra 工程团队在 React 性能规范中评级为CRITICAL关键的第一优先级规则——Promise.all() for Independent Operations对相互独立的异步操作使用Promise.all并发执行。它解决的是 React 应用中最常见的性能杀手一串互不依赖的await被顺序执行形成网络请求瀑布waterfall白白叠加多次网络往返延迟。读完本文你将掌握如何识别可并行的异步调用、如何在 Mastra Playground 这类真实代码库中落地该模式、如何处理并发下的错误与集合扇出以及何时必须保留串行依赖。该规则源自仓库内的最佳实践技能库规则正文见 .claude/skills/react-best-practices/references/rules/async-parallel.md规则优先级与分类见 .claude/skills/react-best-practices/SKILL.md 和 .claude/skills/react-best-practices/references/react-best-practices-reference.md。为什么消除 Waterfall 是九大分类中的第一优先级在 Mastra 的 React 最佳实践中共有 26 条规则、9 个分类而Eliminating Waterfalls消除水瀑效应以 CRITICAL 影响评级排在优先级第 1 位。规则元数据中给出的性能影响描述是2-10× 的提升impactDescription: 2-10× improvement。原因很直接规则目录 react-best-practices-reference.md 中明确写道“Waterfalls are the #1 performance killer. Each sequential await adds full network latency.”——每个顺序await都会完整叠加一次网络延迟一次完整的 RTT请求发出 等待响应往返。当并行执行时多个请求共享同一时间窗口总耗时约等于其中最慢的那一个而非逐个累加。因此在所有优化手段中消除不必要的串行等待带来的收益最大、成本最低。同时SKILL.md 规定该技能应用于“编写新 React 组件、实现数据获取、评审性能问题、重构既有代码、优化包体积与加载时间”等场景并将该规则列在“Critical Patterns (Apply First)”先应用的关键模式清单首位。规则核心独立操作用Promise.all而不是顺序await规则正文的定义非常简洁当异步操作之间没有相互依赖interdependencies时应使用Promise.all()并发执行它们。规则原文给出的反例与正例是// 错误写法顺序执行3 次网络往返3 round trips const user await fetchUser(); const posts await fetchPosts(); const comments await fetchComments();// 正确写法并行执行仅 1 次往返1 round trip const [user, posts, comments] await Promise.all([fetchUser(), fetchPosts(), fetchComments()]);关键在于fetchUser、fetchPosts、fetchComments三个请求彼此不依赖对方的返回值——任何一个都不需要另一个的结果作为入参。此时 JS 引擎依然会逐个await、逐个发起请求、逐个等待响应形成严格的“发起 → 等待完成 → 再发起”的串行链条。改用Promise.all后三个请求几乎同时发出总耗时从三个请求延迟之和收敛为三个请求延迟的最大值。对网络请求而言这一收敛在慢网络高延迟环境下收益最为显著。比如单个请求 RTT 为 200ms顺序执行 3 个请求约需 600ms并行执行则约为 200ms这正是规则元数据声称“2-10× improvement”的现实来源。何时并行、何时必须串行依赖分析是关键Promise.all不是银弹使用前必须判断操作之间是否存在数据依赖。判断标准很简单如果 B 的入参来自 A 的返回值那么 A → B 是强依赖必须串行这种 waterfall 是业务语义决定的、无法消除如果 A、B、C 各自独立仅最终结果需要一起使用那么它们就是Promise.all的候选如果只是“都发出去但不需要聚合结果”甚至可以直接发起后不awaitfire-and-forget但这通常不是 React 数据获取场景的首选。从源码结构看Mastra Playground 中大量遵循了这一判断。以 packages/playground/src/domains/agents/hooks/use-agent-cms-form.ts 第 452-457 行为例发布 Agent 草稿时需要同时读取 Agent 详情和最新版本列表两个请求互不依赖于是并行获取const [agentDetails, versionsResponse] await Promise.all([ client.getStoredAgent(options.agentId).details(), client .getStoredAgent(options.agentId) .listVersions({ orderBy: { field: createdAt, direction: DESC }, perPage: 1 }), ]);注意即使details()与listVersions()都作用于同一个 Agent 实体、返回类型相关只要两者的请求参数和结果不需要互相传递就属于独立操作可以并行。而同一文件中“先保存草稿再基于草稿激活版本”的流程第 443-449 行则是有依赖的串行必须先拿到保存成功的版本响应才能读取latestVersion.id并调用activateVersion。这种依赖链不属于本规则的消除范围。错误处理Promise.all的 fail-fast 与Promise.allSettledPromise.all的语义是快速失败fail-fast只要其中一个 Promise 被 reject整个Promise.all立即 reject且不会等待其他 Promise 的结果。这在“所有请求必须全部成功才有意义”的场景下是合理的——例如上面发布 Agent 的校验逻辑任何一个请求失败都应当中断后续流程并提示用户。但当你的场景是“希望尽量拿到所有结果个别失败不应拖垮整体”时应改用Promise.allSettled。典型例子是 packages/playground/src/domains/agents/hooks/use-agent-cms-form.ts 第 191 行的批量删除 MCP 客户端await Promise.all(mcpClientsToDelete.map(id client.getStoredMCPClient(id).delete()));这里使用Promise.all意味着任一删除失败会导致整体失败——这在批量操作需要“要么全成功、要么全失败”的事务语义时是正确的选择而如果你希望逐个删除、允许部分失败并分别收集结果则应替换为Promise.allSettled后遍历results检查每个status。选择建议所有结果都必需、任一失败即整体失败 →Promise.all需要全部结果、允许个别失败并自行处理 →Promise.allSettled需要按完成顺序流式处理 →Promise.all 结果数组索引即可Promise.all保证结果顺序与输入数组一致。集合扇出Promise.allmap与并发上限控制当独立操作来自一组动态集合而非三个固定的调用时标准写法是Promise.all(arr.map(fn))。仓库中有两处代表性实现一处是批量解析 MCP 客户端详情见 packages/playground/src/domains/agents/hooks/use-agent-cms-form.ts 第 100 行Promise.all(ids.map(id client.getStoredMCPClient(id).details())) .then(results { /* 逐个映射回表单字段 */ });另一处是附件转消息见 packages/playground/src/lib/ai-ui/attachments/composer-attachments.tsx 第 144 行return Promise.all(attachments.map(attachmentToCoreUserMessage));Promise.allmap的代价是所有元素同时并发。当集合规模很小几个 MCP 客户端、几张附件时完全没问题但当集合可能达到成百上千项、且每个请求都打到远端服务时无上限并发可能压垮服务端或被限流。此时应当做分块限流batching把大数组切成固定大小如每次 20 个的块逐块Promise.all块内并行、块间串行。判断依据是集合大小的上界固定且很小个位数直接用Promise.all动态且可能很大上百建议加并发上限。仓库实战印证Mastra Playground 中的并行数据获取最能体现该规则实战价值的例子在 packages/playground/src/domains/experiments/hooks/use-experiment-metrics.ts 第 34-56 行。这是一个 React Query 的queryFn一次要聚合三类实验指标——Token 用量sum、平均耗时avg、运行次数count三者彼此独立于是并行发起queryFn: async (): PromiseExperimentMetrics { if (!experimentId) throw new Error(experimentId is required); const filters { experimentId }; const [tokens, avgDuration, runs] await Promise.all([ client.getMetricAggregate({ name: [mastra_model_total_input_tokens, mastra_model_total_output_tokens], aggregation: sum, filters, }), client.getMetricAggregate({ name: [mastra_agent_duration_ms], aggregation: avg, filters }), client.getMetricAggregate({ name: [mastra_agent_duration_ms], aggregation: count, filters }), ]); return { totalTokens: tokens.value ?? null, estimatedCost: tokens.estimatedCost ?? null, costUnit: tokens.costUnit ?? null, avgAgentDurationMs: avgDuration.value ?? null, agentRuns: runs.value ?? null, }; },这段代码同时展示了该规则的三个要点并行发起三个getMetricAggregate互不依赖一次性并发指标面板渲染前的等待时间约等于最慢的那一个聚合请求解构聚合结果const [tokens, avgDuration, runs] ...Promise.all保证结果数组顺序与输入一致可以直接按位置解构无需额外关联在数据获取层内应用并行发生在queryFn内部对外仍是一个 React Query 查询键[experiment-metrics, experimentId]配合refetchInterval: isActive ? 2000 : false实现实验运行期间的轮询刷新——这正是 SKILL.md 中“依赖查询参数保持严格、用 TanStack Query 管理去重与缓存”等配套规则见 client-request-dedupe 规则的协同用法。另一个值得注意的用法是并行缓存失效。同一文件第 468-474 行在发布 Agent 后一次性失效多个相关查询键await Promise.all([ queryClient.invalidateQueries({ queryKey: [agent-versions, agentId] }), queryClient.invalidateQueries({ queryKey: [stored-agent, agentId] }), queryClient.invalidateQueries({ queryKey: [agent, agentId] }), queryClient.invalidateQueries({ queryKey: [agents] }), queryClient.invalidateQueries({ queryKey: [stored-agents] }), ]);虽然invalidateQueries本身是本地操作、延迟很低但这种写法同样遵循“独立操作并行化”的规则多个互不依赖的缓存刷新一次性触发而不是逐个await串行等待避免了无谓的等待链条。在 React 数据获取中的两种落地姿势结合仓库实践与规则库的整体建议Promise.all在 React 应用中有两种典型落地姿势姿势一在单个queryFn内并行一个查询键聚合多个请求如上文的use-experiment-metrics。适合“一组独立请求共同驱动一个 UI 区域”的场景——一个 hook 返回一个包含多字段结果的对象调用方只关心聚合后的数据。注意保持 hook 返回的 API 窄而明确符合 structure-narrow-apis 规则 对“拆分过大返回对象”的要求。姿势二多个独立useQuery各自请求多个查询键并行适合“多个请求分别驱动不同 UI 区块”的场景。React Query 会为每个查询键独立管理缓存、去重与重新验证多个useQuery同时挂载时会自动并行发出请求无需手动Promise.all——这正是 SKILL.md 中“使用 TanStack Query 实现自动请求去重”的规则client-request-dedupe 规则所覆盖的范畴。两种姿势的选择取决于结果的消费方式统一消费则合并在一个queryFn里用Promise.all分块消费则拆成多个查询键。但无论哪种都应当避免在组件顶层写一连串裸await的顺序请求——那正是本规则要消灭的水瀑。评审清单如何在代码评审中发现 Waterfall把本规则落地为可执行的评审习惯可以对照以下检查项这也是规则文件被设计为“供 Agent 与工程师快速检索”的用途寻找连续裸await一段函数内出现多个顶层await逐一看后面的await是否使用了前面await的返回值。如果后一个请求的入参与前一个请求无关它就是水瀑候选计算往返次数把顺序await数一遍——3 个独立请求顺序执行就是 3 次往返。注释或提交信息中可直接标注“由 3 round trips 收敛为 1 round trip”检查Promise.all的输入是否为独立操作确认列表中的每个 Promise 不依赖列表中的其他 Promise 的结果确认失败语义需要 fail-fast 用Promise.all需要容错用Promise.allSettled不要在Promise.all里写catch后返回undefined来模拟容错这会丢失失败信息且难以定位检查集合扇出的规模Promise.all(arr.map(...))前确认集合规模上界避免无上限并发在数据获取层而非渲染层做并行并行逻辑放在queryFn或 service/hook 内部组件只消费结果保持渲染函数干净。关于测试规则库的整体测试导向是 BDD 风格、只 mock 网络层见 testing-bdd-no-mocks 规则——这意味着你可以在测试中真实驱动并发逻辑通过断言“多个请求同时发出”来验证并行实现而不是用 mock 掩盖请求时序。总结Promise.all()for Independent Operations 是 Mastra React 最佳实践库中评级最高CRITICAL、优先级第 1的规则因为它直接对抗日渐成为前端性能头号杀手的水瀑效应。核心方法论只有一条凡是互不依赖的异步操作就并行执行凡是依赖前序结果的才允许串行。仓库中的 Playground 代码use-experiment-metrics.ts 的指标聚合、use-agent-cms-form.ts 的版本发布与 MCP 批量操作、composer-attachments.tsx 的附件转换是这条规则在真实 React 数据获取场景中的直接印证并行发起、顺序解构、配合 TanStack Query 去重与失效即可在几乎不增加代码复杂度的前提下把多次串行网络往返收敛为一次并行的等待。在编写、评审或重构 React 代码时把它当作第一优先级检查项——先消除 Waterfall再谈其他优化。【免费下载链接】mastraMastra is the modern TypeScript framework for AI-powered applications and agents.项目地址: https://gitcode.com/GitHub_Trending/ma/mastra创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考