What you'll learn
- Paths for sequential awaits, N+1 loops, tree waterfalls and early-exit awaits
- Match Network smells to the right fix
- Four rules with before and after code
- Quick reference table for common patterns
Async waterfalls happen when we await operations one by one even though they could run at the same time. In most codebases this starts innocently: a handler grows, another fetch is added, then another, and now each request waits for the previous one even when there is no real dependency.
The cost is measurable and repeated across your entire user base. A 600ms waterfall vs 200ms parallel path is not a one-time curiosity; every session pays this tax. Removing waterfalls is one of the biggest performance wins you can get without changing infrastructure.
Part of the Performance & Optimization cluster. Use the path picker when you know the shape. Use the smell matcher when Network waterfalls look confusing.
Impact: lower latency on every request path

Pick a waterfall fix path
Try it: waterfall fix path
Pick the waterfall shape you have. You get an ordered approach, why it fits, and a first-week plan.
Your path
Collapse independent awaits with Promise.all
If calls do not depend on each other, wall-clock time should be the max of the calls, not the sum.
- Parallelize independent awaits
Start independent work together.
- Promise.all
Wait for the batch once.
- React Performance Optimization
Measure load and runtime after the change.
- JavaScript Performance Patterns
Keep JS loops cheap after fetch returns.
First week
Find one handler with three sequential fetches. Convert independents to Promise.all and compare Network timing.
Only parallelize calls that are truly independent. Dependent chains stay sequential on purpose.
Checkpoint
Three fetches in a dashboard loader do not depend on each other. What should you do?
Pick an answer, or press A, B, C on your keyboard.
Match a waterfall smell

Try it: match a waterfall smell
Choose the smell you see in Network or code review. The matcher points to a primary fix.
The smell
Each iteration awaits a network call before the next ID starts.
Start here
Avoid N+1Deduplicate IDs and Promise.all the unique fetches so list size does not multiply latency.
A 600ms waterfall vs a 200ms parallel path is a tax every session pays.
Key takeaway
Only parallelize calls that are truly independent. Dependent chains stay sequential on purpose.
Rule 1: Defer async calls past early-exit conditions
When a branch can return early, evaluate that branch before starting expensive async work.
The bug is subtle: the await ran even when its result was never used. Early returns are free wins for both latency and resource usage.
Rule 2: Parallelize independent awaits

If calls do not depend on each other, run them concurrently.
// Timing diagram:
// Sequential: 300ms + 300ms + 300ms = 900ms
// Parallel: max(300ms, 300ms, 300ms) = 300msRule 3: Avoid N+1 patterns

A common waterfall appears inside loops: each iteration awaits before moving to the next.
Real-world example: fetching author details for a list of post cards. The looped await becomes a latency multiplier as list size grows.
Rule 4: Component-level waterfalls
Classic React waterfall:
- Parent fetches data
- Parent renders child
- Child starts its own fetch after mount
This serializes work across component boundaries.
Route-level orchestration keeps the fetch graph explicit and prevents mount-time request chains.
Streaming and Suspense can paint earlier, but nested fetch-on-reveal can still serialize. See React Rendering Strategies for where data can start earlier.
Once data is fetched on the server, how it is cached decides how fresh the page is. Update Content Without Redeploying covers no-store, revalidation and the caches in between.
Checkpoint
You hydrate authors inside a for-loop with await fetchUserById. What is the better shape?
Pick an answer, or press A, B, C on your keyboard.
Quick Reference
| Pattern | Problem | Fix | Impact |
|---|---|---|---|
| Early async before branch | Work runs even when function exits early | Move early-exit checks before await | Removes wasted network or database calls |
| Sequential independent awaits | Total latency adds up (A + B + C) | Use Promise.all for independent calls | Collapses wall-clock time to max single call |
| N+1 await in loops | Each iteration blocks the next | Gather IDs, batch with Promise.all, map results | Scales better with list size |
| Parent/child fetch chain | Child fetch starts only after parent render | Fetch in route, page or layout and pass props | Eliminates component-level waterfalls |
The key habit
Draw the dependency graph. Parallelize the independent edges. Keep true dependencies sequential.
Waterfall checklist
- Cheap guards run before expensive awaits
- Independent fetches start together
- Loops do not await per item when a batch is possible
- Parent and child do not serialize mount-time fetches without a reason
- Suspense boundaries are not accidental serial fetch gates
- I re-measured Network timing after the change
Further reading
- React Performance Optimization
- React Rendering Strategies
- Update Content Without Redeploying
- JavaScript Performance Patterns
- Bundle Size Optimization
Conclusion
Waterfalls are usually accidents of growth, not requirements. Make dependencies explicit, then collapse the rest.
The key takeaway
Independent work should finish in the time of the slowest call, not the sum of all calls.
Check your understanding
Three independent dashboard fetches are sequential today. What should you do?
Pick an answer, or press A, B, C on your keyboard.
Recap
What to remember from this article
Sections read
0
Questions
0
none answered yet
Quiz
0
not taken yet
Up next: Bundle Size Optimization · 11 min
ContinueUp next: React Performance & Optimization