Stream with Suspense

Give each slow section its own <Suspense> so fast ones render first.

From next-best-practices, data-patterns.md.

Three boundary layouts

Press Load, or drag the time scrubber.

No boundaryBad
–
loading.tsx onlyBetter
–
One per sectionGood
–

// app/dashboard/page.tsx
import { Suspense } from 'react'

export default function Dashboard() {
  return (
    <main>
      <Suspense fallback={<UserSkeleton />}>
        <UserSection />
      </Suspense>
      <Suspense fallback={<PostsSkeleton />}>
        <PostsSection />
      </Suspense>
    </main>
  )
}

async function UserSection() {
  const user = await getUser()
  return <UserCard user={user} />
}

Place the boundaries

Click a gap to split or merge, then press Stream.

Page0 ms
Timeline
Skeletons
4
First content
–
Ready but hidden
–

Size the fallback

Press Stream posts and watch the Reply box.

"Loading…"Bad
–
<PostsSkeleton />Good
–
function PostsSkeleton({ rows = PAGE_SIZE }) {
  // same row height and count as <PostList>
  return (
    <ul aria-hidden className="posts">
      {Array.from({ length: rows }, (_, i) => <li key={i} className="post skeleton" />)}
    </ul>
  )
}

<Suspense fallback={<PostsSkeleton />}>
  <PostsSection />
</Suspense>

Why it matters, pros and cons

Why it matters

Without boundaries, nothing ships until the slowest await resolves.

Pros

  • Fast sections never wait on slow ones.
  • Each section owns its own fetch.
  • loading.tsx gives a route boundary free.

Cons

  • Every boundary adds a skeleton and a later shift.
  • Mismatched fallback heights cause layout shift.
  • The first flush locks the HTTP status.