Chapter 34: Performance & Request Waterfalls
Core Idea
A request waterfall is any resource fetch that can't start until a prior, unrelated fetch finishes — the single biggest performance footgun in component-driven data fetching — and the fix is almost always structural: flatten the API, hoist/parallelize queries, or prefetch ahead of render.
Key Concepts
- What a waterfall is: sequential round trips where step N can't begin until step N-1 resolves. Each hop costs at least one round trip of latency — on a high-latency connection (e.g. ~250ms), a 4-hop waterfall costs roughly 1000ms of pure latency versus ~500ms for a flattened 2-hop version.
- Baseline waterfall: even without any dependent queries, there's already an implicit markup → JS → first query chain before any data can render — this is the floor, not something a single query call can remove.
- Single-component / serial queries: a dependent query (second query needs data from the first, gated by
enabled) is a textbook waterfall — fix by combining backend endpoints (e.g. one getProjectsByUserEmail instead of getUserByEmail then getProjectsByUser) when feasible, or accept the latency cost when it isn't.
- Suspense's serial trap: multiple
useSuspenseQuery calls side by side in one component run in serial, not parallel, because the first one suspends the component before the others execute — useSuspenseQueries (an array of query configs, one hook call) is the fix, restoring parallelism.
- Nested component waterfalls: a parent query gating a child's render, where the child's own query then only starts after the parent resolves. If the child's query doesn't actually depend on parent data (just receives an already-available prop), hoist the child's query up to the parent and fetch both in parallel (or combine into
useSuspenseQueries under Suspense) — this is a non-dependent nested waterfall, the cheap case to fix. A genuinely dependent nested waterfall (child needs an id computed from parent data, and only renders conditionally) usually needs an API restructure or a more advanced technique (like moving the fetch to a Server Component) rather than a simple hoist.
- Code splitting compounds waterfalls: lazy-loading a component that itself contains a query adds a "load the JS chunk" hop before that component's query can even start, stacking on top of any data waterfall already present — a full first-page-load chain can easily reach five sequential round trips (markup → parent JS → parent query → child JS chunk → child query). Mitigating this means trading bundle size for latency: hoisting/conditionally prefetching the child's query into the parent's bundle removes the hop but grows the main bundle.
Key Takeaways
- Treat request waterfalls as a structural problem, not a query-configuration one — the durable fixes are API redesign (combined endpoints), query hoisting/parallelization, and prefetching, not a
useQuery option.
- Swap side-by-side
useSuspenseQuery calls for one useSuspenseQueries call the moment more than one Suspense query lives in the same component — otherwise they silently serialize.
- Distinguish dependent from non-dependent nested waterfalls before reaching for a fix: non-dependent ones are a cheap hoist; genuinely dependent ones usually need an API change or server-side restructuring.
- Periodically audit the Network tab for waterfalls — they accumulate silently as components get refactored, moved, or split without anyone noticing the new dependency chain.
Connects To
- Dependent Queries: the canonical single-component waterfall this chapter generalizes.
- Prefetching & Router Integration: the primary tool for flattening waterfalls without an API redesign.
- Advanced Server Rendering: how Server Components move some of this cost server-side, where latency is lower.