writing/nested-suspense-controls-reveal-order
WebJun 30, 2026·6 min read

Nested Suspense: control the order your content reveals

Nested Suspense decides the order your sections appear, not when their data is fetched, and it will not save you from a waterfall.

Adrian Szabłowski
Adrian SzabłowskiAI web developer at Tidio
Three stacked horizontal panels made of glowing amber particles streaming in from the left, the top panel fully lit, the middle one resolving with a teal accent, the bottom one still faintly forming

You have seen the chaotic version of a loading page. The footer appears before the hero. A sidebar widget beats the main article to the screen. Skeletons fill in like popcorn, and for a second the page looks broken even though every request succeeded. The fix people reach for is to nest their <Suspense> boundaries, on the belief that nesting imposes order and dodges waterfalls. Half of that is true. The other half is the most common thing I see developers get wrong about Suspense.

So I built a small Next.js demo with two routes that render identical data with identical delays, and differ only in how the boundaries are arranged. It made the distinction obvious enough that I want to walk through it.

A 30-second streaming recap

In traditional server-side rendering the server builds the entire HTML document before it sends a single byte, so one slow query blocks the whole page. Streaming changes that. React's server renderer produces HTML in chunks aligned with <Suspense> boundaries, and Next.js sends the static shell first: your layout, navigation, and every Suspense fallback. Each boundary then streams in as its own data resolves. The bundled Next.js docs put it plainly: "Each <Suspense> boundary is an independent streaming point."

That independence is the whole game. Where you draw the boundaries decides what the user sees first, and in what order. Two arrangements of the same boundaries produce two very different experiences.

The demo: two routes, one data source

Both routes pull from the same function. It returns a fixed list of posts after a random half-second to three-second delay, so each request simulates the unknown timing of a real backend.

// app/_lib/posts.ts
import { connection } from 'next/server';
 
export async function getPosts(section: string) {
  await connection();
  const delayMs = 500 + Math.floor(Math.random() * 2500);
  await new Promise((resolve) => setTimeout(resolve, delayMs));
  return { posts: SECTIONS[section] ?? [], delayMs };
}

Each section renders a small "data resolved in 1.4s" caption, so you can compare when the data arrived against when the section actually appeared. On one of the routes those two times stop matching, and that gap is the point of the whole exercise.

Siblings: fast, parallel, whatever order the network picks

The first route places three boundaries side by side.

// /separate: sibling boundaries
<Suspense fallback={<Skeleton />}>
  <PostList section="featured" />
</Suspense>
<Suspense fallback={<Skeleton />}>
  <PostList section="recent" />
</Suspense>
<Suspense fallback={<Skeleton />}>
  <PostList section="popular" />
</Suspense>
Sibling boundaries. Each section reveals as its own data lands, so the order changes on every reload.

Each boundary is independent, so each section reveals the moment its own data is ready, regardless of where it sits in the document. On one reload, Popular resolved in about 0.6s and Recent in about 0.65s, and both painted while Featured was still spinning at the top, which did not arrive until roughly 2.95s. Reload again and the order is different, because the order now tracks data speed rather than your markup. This is the fastest arrangement for perceived completion, since no section ever waits on another. It is also the one that produces the popcorn effect when the sections belong to a visual hierarchy.

Nested: same data, always top-down

The second route nests the same three boundaries inside each other.

// /nested: nested boundaries
<Suspense fallback={<Skeleton />}>
  <PostList section="featured" />
  <Suspense fallback={<Skeleton />}>
    <PostList section="recent" />
    <Suspense fallback={<Skeleton />}>
      <PostList section="popular" />
    </Suspense>
  </Suspense>
</Suspense>
Nested boundaries. Same data, same delays, but the reveal is always top-down: Featured, then Recent, then Popular.

Now Featured always reveals first, then Recent, then Popular, on every single reload. React will not paint a nested child before its parent has painted, so the bottom of the tree can never beat the top to the screen. The "resolved in" captions expose the cost: a section that fetched quickly can sit there finished but hidden, waiting for its slower parent to clear. That is the trade. You give up some perceived speed in exchange for an order the network can no longer scramble.

Nesting does not avoid waterfalls

It is tempting to read the nested cascade and conclude that nesting forces a staged, one-after-another sequence, and that it prevents waterfalls along the way. Neither is true.

Nesting only guarantees a floor. A child cannot reveal before its parent. It does not force a child to reveal after. If a nested child's data is already done when its parent resolves, the two paint together. I checked this by giving all three nested sections an equal two-second delay. They did not cascade. All three appeared at once at about the two-second mark, in a single chunk. The 1s, 2s, 3s staircase I had seen earlier came entirely from the staggered delays I happened to set, not from the nesting. Real arrival times decide the gaps between reveals. Nesting only decides that the gaps, if any, run top-down.

A data-fetch waterfall is a different thing, and it is about timing, not order. It happens when one request cannot start until another finishes, usually because you await data in a parent before rendering the child that fetches next, or because of a genuine data dependency between them. Rearranging Suspense boundaries never changes when the requests fire, so nesting cannot remove a waterfall. In this demo both routes finish in the time of the single slowest fetch rather than the sum of all three, because every section fetches its own data independently. The boundaries only decide the order those results show up.

The Network tab will not show this section by section. A streaming page is one response, so you see a single request whose body arrives over time, not one entry per section. The 2.2s you might see there is the slowest fetch, which is also good proof the sections load in parallel. Judge reveal order by what actually paints in the browser, with DevTools or a screen recording, rather than by the order or count of network entries.

When to reach for each

Sibling boundariesNested boundaries
RevealEach section the instant it is readyStrict top-down, parent before child
OrderChanges per reload, network decidesAlways the same, regardless of timing
Perceived speedFastest, no section waits on anotherSlower, a fast child waits on its parent
FetchingParallelParallel, identical to siblings
Best forIndependent widgets where any order is fineHierarchical content where pop-in looks broken

Reach for siblings when the sections are genuinely independent and any arrival order reads as fine, like a dashboard of unrelated cards. Reach for nesting when the content has a hierarchy a reader expects in sequence, like a product header before its details before its reviews, and out-of-order reveal would look like a bug.

The model worth keeping is short. Boundaries decide the order sections appear. The data's own arrival time decides how fast each one gets there. Once you stop expecting nesting to fix timing, you can pick the reveal order on purpose instead of letting the network pick it for you.

// KEEP READING

Command palette

Search pages, posts, and actions