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.
Page
Timeline
- Skeletons
- 4
- First content
- –
- Ready but hidden
- –
Size the fallback
Press Stream posts and watch the Reply box.
"Loading…"Bad<PostsSkeleton />Goodfunction 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.tsxgives 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.