Request Waterfall 与并行数据获取
区分 accidental waterfall 与真实依赖链,并用 Promise.all、组件拆分与 Suspense/Streaming 缩短互不依赖请求的关键路径。
[!info] related notes
- 所属 MOC:
- 相关概念:
- 易混淆概念:
- 相关资源:
Request Waterfall 与并行数据获取
覆盖范围
这篇笔记讨论多个异步数据请求之间的执行关系:什么时候顺序 await 会制造不必要的 Request Waterfall,什么时候顺序又是业务依赖所必需,以及 Promise.all、组件拆分和 Suspense 如何缩短等待关键路径。
什么是 Request Waterfall
如果三个互不依赖的请求被连续等待:
const profile = await getProfile()
const assessments = await getAssessments()
const notifications = await getNotifications()
执行形态接近:
getProfile
████████
getAssessments
████████
getNotifications
████████
总等待时间接近三个请求耗时之和。
这种“后一个请求只是因为代码写在后面才开始”的串行等待,是典型 accidental waterfall。
Independent Requests 应并行
如果请求互不依赖,可以先同时启动:
const [profile, assessments, notifications] = await Promise.all([
getProfile(),
getAssessments(),
getNotifications(),
])
执行形态:
getProfile ████████
getAssessments ██████████
getNotifications █████
总等待时间更接近最慢请求,而不是全部相加。
Promise.all 的语义
Promise.all 适合“所有结果都必须成功才能继续”的组合:
A、B、C 同时开始
↓
全部成功
↓
继续
其中任意一个 Promise reject,Promise.all 会 reject。
如果希望每个请求独立报告成功/失败,可以考虑 Promise.allSettled(),但这并不自动意味着 UI 应忽略失败;仍要根据业务语义决定降级策略。
Dependency Waterfall 是合理的
并非所有 waterfall 都是性能 Bug。
例如:
const user = await getCurrentUser()
const organization = await getOrganization(user.organizationId)
const projects = await getProjects(organization.id)
这里后一步真的依赖前一步结果:
user
↓ supplies organizationId
organization
↓ supplies org id/context
projects
这种是 dependency waterfall。
核心区别:
Accidental waterfall
= 代码顺序造成的等待
Dependency waterfall
= 数据依赖造成的等待
优化目标是消除前者,而不是机械消灭所有顺序请求。
并行不等于页面一定更快出现
假设:
Profile 50 ms
Assessments 100 ms
AI Insight 5 s
即使:
await Promise.all([
getProfile(),
getAssessments(),
getAIInsight(),
])
如果页面必须等三个全部完成,仍然会被 5 秒的请求阻塞。
所以性能问题有两个不同维度:
请求何时启动?
→ parallelization
页面是否必须等它?
→ boundary / streaming
与 Suspense / Streaming 的关系
对于独立且明显更慢的区域,可以让它拥有自己的渲染边界:
<Profile />
<Assessments />
<Suspense fallback={<AIInsightSkeleton />}>
<AIInsight />
</Suspense>
形成:
快数据完成
→ 先显示
慢数据
→ fallback
→ 后续 Streaming 补齐
因此:
Promise.all
解决“别无故串行”
Suspense / Streaming
解决“别让慢区域阻塞所有快区域”
Next.js 对应机制见 Next.js Loading 与 Streaming。
请求可以在更低层启动
有时顶层 Page 不必统一等待所有数据。通过 数据获取共置,不同 Server Component 可以自行请求各自数据,再由 Suspense 定义等待边界。
Dashboard
├── ProfilePanel
│ └── getProfile()
├── AssessmentPanel
│ └── getAssessments()
└── AIInsightPanel
└── getAIInsight()
这可以减少“顶层拿完所有数据才开始渲染”的人为集中阻塞。
BodySense / Dashboard 例子
如果 Dashboard 同时需要:
getProfile()
getAssessments()
getNotifications()
且三者互不依赖,应优先:
const [profile, assessments, notifications] = await Promise.all([
getProfile(),
getAssessments(),
getNotifications(),
])
如果未来某个 AI summary 明显更慢,可以进一步拆成独立 Suspense boundary,而不是把它塞进同一个必须完成的 Promise.all 后才展示整页。
常见误区
“所有连续 await 都错”
错。真实数据依赖必须按顺序执行。
“用了 Promise.all 就没有性能问题”
错。最慢请求仍然可能成为整个页面的 blocking critical path。
“组件拆开后请求一定并行”
不应只凭组件层级猜执行时序;关键仍然是 Promise 何时创建、框架如何调度,以及 Suspense boundary 如何组织等待。
“Waterfall 只发生在浏览器 Network 面板”
不是。服务器内部 API、数据库和服务调用也可能形成逻辑 waterfall,即使浏览器只看到一个页面请求。
最短记忆
无依赖
→ 尽早同时启动
→ Promise.all
有真实依赖
→ sequential 合理
一个特别慢
→ Suspense / Streaming 隔离