skip to content

A Next.js App Router page renders in about 1.8 seconds. Its server component awaits `getUser()`, then `getOrders()`, then `getRecommendations()`, and none of the three depends on the others. Why is the page slow, and how do you fix it?

level: seniorimportance: should knowfreq 56%

answer

  1. sum versus maximum
  2. the client waterfall left, the server one stayed
  3. calling starts the work, awaiting blocks on it
  4. parents block children that fetch
  5. only real data dependencies must be serial

basics

~20 s

Three independent awaits in sequence cost the sum of their latencies, not the maximum. Start all three calls before awaiting any of them and combine with Promise.all, so the page waits roughly as long as the slowest one instead of all three added together.

solid answer

~50 s

Moving data access into Server Components removes the *client-side* waterfall but does nothing about a server-side one. `await` suspends the function, so each request only leaves the server after the previous response arrives, and the page pays the sum. Since the three calls are independent, kick them off first and await them together with `Promise.all`, which makes the page cost roughly the slowest call. The other flavour of this bug is structural: a parent Server Component that awaits before rendering a child that awaits its own data, because the child's request cannot start until the parent's resolves. There the fix is to make the fetches siblings, or to start the promise in the parent and let the child await it. A genuinely dependent call — you need the user's id to fetch their orders — is not a bug and cannot be parallelised.

code

typescript · 11 lines
typescript
// Sequential: total wait is the SUM of the three latencies
const user = await getUser(id)
const orders = await getOrders(id)
const recs = await getRecommendations(id)

// Overlapped: total wait is roughly the SLOWEST of the three
const [user2, orders2, recs2] = await Promise.all([
  getUser(id),
  getOrders(id),
  getRecommendations(id),
])

go deeper

for a junior

Recall that await pauses until the promise settles, so three awaits in a row take as long as all three added up, and that Promise.all is the tool for independent work.

for a middle

Explain that invoking an async function starts the work while await only chooses when to block, and show the rewrite that starts all three calls before awaiting the set.

for a senior

Diagnose from evidence — compare summed call durations against total render time — and distinguish the in-function waterfall from the structural parent-blocks-child one, naming the promise-passing or sibling restructure for the latter.

for a principal

Own the standard: a rule for how nested data dependencies are expressed, guidance on when a genuine dependency should be collapsed into one query, and instrumentation that makes serial fetching visible in review rather than in a user complaint.

## The waterfall did not go away, it moved The headline benefit of fetching in Server Components is that data no longer waits for the JavaScript bundle to download, parse and hydrate before the first request is made. That kills the *client* waterfall. It does not make your server code concurrent. `await` is still `await`: it suspends the async function until the promise settles, so three sequential awaits are three round trips end to end, and the page's time to first byte is their sum. ```ts // ~600ms + ~700ms + ~500ms ≈ 1.8s const user = await getUser(id) const orders = await getOrders(id) const recs = await getRecommendations(id) ``` ## Fix one: overlap independent work Calling an async function starts its work immediately; `await` only decides when you block on the result. So start all three, then await the set: ```ts // ≈ 700ms — the slowest of the three const [user, orders, recs] = await Promise.all([ getUser(id), getOrders(id), getRecommendations(id), ]) ``` Use `Promise.allSettled` instead when one of the three is optional and you would rather render a degraded panel than fail the route: `Promise.all` rejects as soon as any input rejects, and in a Server Component that rejection propagates as a render error for the whole segment. ## Fix two: the structural waterfall The subtler case has nothing to do with `await` ordering inside one function. If a parent Server Component awaits its own data before returning JSX, its children do not exist yet — React has not called them — so a child that fetches its own data cannot start until the parent has finished. The result is a chain that no `Promise.all` inside any single component can flatten, because the calls live in different components. Three ways out, in increasing order of restructuring: - **Hoist.** Fetch both in the parent with `Promise.all` and pass results down as props. Simple, but it re-centralises data access and grows the parent's prop surface. - **Make them siblings.** If the child does not actually need to be nested under data the parent fetched, render both as siblings so React starts both renders in the same pass. - **Start early, await late.** Call the child's data function in the parent *without* awaiting it, and pass the promise down for the child to await. The request is in flight during the parent's own wait. Next documents this as the preload pattern: a small `preload()` helper that calls the data function and discards the result (`void getItem(id)`) purely to warm it before the tree that needs it renders. ## Which waits are real Not every sequence is a defect. If you need the account id from `getUser()` to call `getOrders(accountId)`, that dependency is genuine and no amount of restructuring removes it — you can only make the round trips cheaper (a single query that joins, a batched endpoint, a co-located datastore). The diagnostic question is always: *does call B need a value produced by call A?* If not, it should not be waiting for it. ## Streaming is a different lever Streaming does not make the awaits concurrent — it changes when the user sees the parts that are ready. It is worth reaching for once the parallelisation is done and one section is still legitimately slow: isolate that section so the rest of the page is not held behind it. But reaching for streaming *instead* of fixing sequential awaits just means the user watches placeholders for the same 1.8 seconds; the total work is unchanged. ## How to actually see it Do not eyeball the code. Instrument: wrap each data function with timing, or use a tracing integration, and compare the sum of individual call durations against the route's total server render time. If total ≈ sum, everything is sequential. If total ≈ max, you are already parallel and the remaining cost is one slow dependency, which is a different conversation — indexes, payload size, or the vendor's latency, not your `await` ordering. ## The anti-pattern to name A route where each of five Server Components independently calls the same `getUser()` is not a waterfall, but it is the neighbouring pathology: fan-out. Waterfalls cost you latency in series; fan-out costs you load in parallel. Fixing one by causing the other is a common own goal.

  • Why does wrapping the component in a Suspense boundary not fix this?
    A boundary changes what the user sees while the work runs; it does not change the work. The three awaits still execute one after another inside the same function, so the section takes the same total time — the user just watches a fallback for it. Parallelise first, then stream what remains genuinely slow.
  • When would you prefer Promise.allSettled over Promise.all here?
    When one of the calls is optional. `Promise.all` rejects on the first rejection, and an unhandled rejection in a Server Component fails the render for that segment. `allSettled` always resolves with per-promise status, so you can render the recommendations panel as empty or degraded while still showing the user and their orders.
  • A child component needs data the parent does not use. How do you keep the parent from blocking it?
    Start the request in the parent without awaiting it and pass the promise down for the child to await, so it is in flight during the parent's own wait. Alternatively restructure so the two components are siblings rather than nested, which lets React begin both renders in the same pass.

saying these in an interview costs you the question

  • Says server-side fetching removes waterfalls entirely
  • Thinks await runs the calls concurrently by itself
  • Reaches for streaming instead of parallelising the calls
  • Parallelises calls that genuinely depend on each other's results
  • Believes a parent's await does not delay a child's fetch

context