← Back

Streaming with Suspense

Streaming with Suspense boundaries, co-locating skeleton components with their data.

PublishedMarch 5, 2026119 words
Edit

Suspense and Streaming

Suspense specifies loading UI while async content loads, enabling streaming in Next.js.

Example: Dashboard

From app/dashboard/page.tsx:

export default function DashboardPage({ searchParams }) { return ( <div> <Suspense fallback={<PostTabsSkeleton />}> <PostTabs /> </Suspense> <Suspense fallback={<PostListSkeleton />}> <PostList searchParams={searchParams} /> </Suspense> </div> ); }
export default function DashboardPage({ searchParams }) { return ( <div> <Suspense fallback={<PostTabsSkeleton />}>

Separate boundaries let each section stream independently.

Co-locating Skeletons

From app/dashboard/_components/PostList.tsx:

export async function PostList({ searchParams }) { const posts = await getPosts(filter); return posts.map(post => <Card key=

Export skeletons alongside their components to keep them in sync.

<
PostTabs
/>
</
Suspense
>
<
Suspense
fallback
=
{
<
PostListSkeleton
/>
}
>
<
PostList
searchParams
=
{
searchParams
}
/>
</
Suspense
>
</
div
>
)
;
}
{
post
.
slug
}
>
...
</
Card
>
)
;
}
export
function
PostListSkeleton
(
)
{
return
(
<
div
className
=
"
space-y-4
"
>
{
[
1
,
2
,
3
]
.
map
(
i
=>
(
<
Card
key
=
{
i
}
>
<
Skeleton
className
=
"
h-6 w-48
"
/>
<
Skeleton
className
=
"
h-4 w-24
"
/>
</
Card
>
)
)
}
</
div
>
)
;
}
export async function PostList({ searchParams }) { const posts = await getPosts(filter); return posts.map(post => <Card key={post.slug}>...</Card>); } export function PostListSkeleton() { return ( <div className="space-y-4"> {[1, 2, 3].map(i => ( <Card key={i}> <Skeleton className="h-6 w-48" /> <Skeleton className="h-4 w-24" /> </Card> ))} </div> ); }