← [ BACK_TO_NOTES ]

Request Waterfall 与并行数据获取

区分 accidental waterfall 与真实依赖链,并用 Promise.all、组件拆分与 Suspense/Streaming 缩短互不依赖请求的关键路径。

#synthesis #growing #frontend #javascript

[!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 隔离
创建于 2026/9/15 更新于 2026/9/15
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本