Lesson 3 of 30
A request waterfall is what happens when requests that never actually depended on each other still run one after another anyway, and it's a real, common performance bug — not a rare edge case.
13 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
What's the concrete, visual way to actually spot a request waterfall in a real running app, rather than just reasoning about it from reading code?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A request waterfall happens when a parent component fetches data, and only after that fetch resolves does a child component render and start its own fetch — which resolves before a grandchild starts its own. Each request waits for the previous one to fully finish before it even starts, even though several of them never actually needed data from each other. The total load time becomes the sum of every request's duration, when it could have been closer to the slowest single request if they'd run together.
This usually isn't a deliberate design choice — it's an accident of component structure: data fetching gets placed at the point in the tree where it's convenient to write, not at the point where it's actually required.
// Waterfall: each fetch only starts after its parent's fetch resolves
function ProfilePage({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
if (!user) return <Spinner />;
return <ProfilePosts userId={user.id} />;
}
function ProfilePosts({ userId }: { userId: string }) {
const [posts, setPosts] = useState<Post[] | null>(null);
useEffect(() => {
fetchPosts(userId).then(setPosts);
}, [userId]);
if (!posts) return <Spinner />;
return <ProfileStats userId={userId} postCount={posts.length} />;
}
// ...ProfileStats fetches a third thing, only after posts resolve
// Parallel: independent requests fire together, awaited together
function ProfilePage({ userId }: { userId: string }) {
const [state, setState] = useState<{ user: User; posts: Post[]; stats: Stats } | null>(null);
useEffect(() => {
Promise.all([fetchUser(userId), fetchPosts(userId), fetchStats(userId)]).then(
([user, posts, stats]) => setState({ user, posts, stats })
);
}, [userId]);
if (!state) return <Spinner />;
return <ProfileView {...state} />;
}
Three requests, waterfall style: user first, then posts only after user resolves, then stats only after posts resolves.
fetchPosts and fetchStats never actually needed data from fetchUser in this example — they only needed the same userId the page already had. Firing all three together with Promise.all (or lifting the fetches to a shared parent, or using a data-fetching library's parallel-query capability) turns three sequential round-trips into one wait for the slowest of the three.
Promise.all manually. If a parent already needs to fetch for its own reasons, having it fetch the children's data too and pass it down as props sidesteps the child components needing their own effects at all.Promise.all, some fetching libraries let multiple components each declare their own query and the library coordinates firing them concurrently without a shared parent effect."How would you find and fix a page that's loading slower than it should?" is a real, common performance question, and naming the Network tab's staircase pattern as the concrete diagnostic — then explaining the Promise.all or lift-and-pass-down fix — shows hands-on debugging experience, not just textbook knowledge of the term "waterfall."