Streaming with Suspense
Without Suspense, users see blank screens while data loads. Each database query or API call blocks the entire page from rendering. With Suspense, you design what users see during loading—skeleton placeholders that communicate progress, not emptiness.
Each Suspense boundary is a UX decision: "What should users see while this section loads?" And importantly: "Should these sections wait for each other, or load independently?"
Independent Streaming
Separate boundaries let sections stream in parallel. The tabs can appear while posts are still loading:
import { Suspense } from 'react';
export default function DashboardPage({ searchParams }) {
return (
<div>
<Suspense fallback={<PostTabsSkeleton />}>
<PostTabs />
</Suspense>
<Suspense fallback={<SortButtonSkeleton />}>
<SortButton />
</Suspense>
<Suspense fallback={<PostListSkeleton />}>
<PostList searchParams={searchParams} />
</Suspense>
</div>
);
}Each component fetches its own data. As each resolves, it streams in and replaces its skeleton—independently of the others.
Boundary Placement Strategy
How you group components inside Suspense boundaries affects perceived performance:
| Scenario | Strategy | Effect |
|---|---|---|
| Related content | Single boundary | Load together, show together |
| Independent sections | Separate boundaries | Stream in parallel as ready |
| Critical UI (header, nav) | Outside boundaries | Always visible immediately |
| Heavy content | Own boundary | Don't block lighter content |
Nested Boundaries
Suspense boundaries can nest. Inner boundaries resolve first:
<Suspense fallback={<PageSkeleton />}>
<Header />
<Suspense fallback={<ContentSkeleton />}>
<Content />
</Suspense>
</Suspense>The outer skeleton shows briefly, then the header appears, then content streams in. Users see progressive disclosure.
Transitions Keep Content Visible
Here's a crucial distinction: Suspense fallbacks appear on initial load. During subsequent navigations wrapped in startTransition, React keeps existing content visible instead of showing fallbacks again.
// Using Link or router.push - content stays visible during navigation
<Link href="/dashboard?filter=drafts">Drafts</Link>
// The PostList re-fetches but old content shows until new data arrivesThis is why filtering and sorting feel instant even though data is refetching—the skeleton only shows on first load. For subsequent interactions, the current list stays visible with a subtle pending state.