Next.js Streaming and Suspense
Stream pages with Suspense and loading files so users see content before slow data arrives.
TL;DR
- Add
loading.tsxto show a fallback while a route loads. - Wrap slow components in
<Suspense>to stream them in later. - Enable
cacheComponentsto prerender a static shell around dynamic parts.
Loading Files
loading.tsxShow instant fallback UI for a whole route segment while its page loads.
// app/dashboard/loading.tsx
export default function Loading() {
return <p>Loading dashboard...</p>;
}Skeleton UIMirror the shape of the real content with simple placeholder blocks.
export default function Loading() {
return (
<div className="animate-pulse space-y-3">
<div className="h-6 w-1/3 bg-gray-200" />
<div className="h-40 bg-gray-200" />
</div>
);
}Nested LoadingAdd loading files at different levels to scope each fallback to its segment.
app/
loading.tsx // whole app
dashboard/
loading.tsx // dashboard only
page.tsxSuspense Boundaries
<Suspense>Wrap one slow component and show a fallback only in that spot.
import { Suspense } from 'react';
<Suspense fallback={<p>Loading reviews...</p>}>
<Reviews id={id} />
</Suspense>Separate BoundariesGive each slow section its own boundary so they load independently.
<Suspense fallback={<ChartSkeleton />}>
<SalesChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<OrdersTable />
</Suspense>Fetch in the ChildFetch inside the suspended component so its parent never has to wait.
async function Reviews({ id }: { id: string }) {
const reviews = await getReviews(id);
return <p>{reviews.length} reviews</p>;
}Static Shell and Dynamic Parts
cacheComponentsEnable Partial Prerendering so a prebuilt static shell is served instantly.
// next.config.ts
const nextConfig = {
cacheComponents: true,
};
export default nextConfig;Runtime DataWrap components that read cookies or headers in Suspense so they stream in.
import { cookies } from 'next/headers';
async function Greeting() {
const name = (await cookies()).get('name')?.value;
return <p>Hello, {name ?? 'guest'}</p>;
}connection()Defer a component to request time when it needs a fresh value per visit.
import { connection } from 'next/server';
async function Now() {
await connection();
return <p>{new Date().toLocaleTimeString()}</p>;
}Streaming to the Client
Pass a PromiseStart a request on the server without awaiting it, then pass the promise down.
export default function Page() {
const comments = getComments(); // no await
return (
<Suspense fallback={<p>Loading...</p>}>
<Comments data={comments} />
</Suspense>
);
}use()Unwrap the streamed promise inside a Client Component with the use hook.
'use client';
import { use } from 'react';
export function Comments({
data,
}: {
data: Promise<Comment[]>;
}) {
const comments = use(data);
return <p>{comments.length} comments</p>;
}error.tsxCatch a failure in a streamed section without breaking the rest of the page.
'use client';
export default function Error() {
return <p>Could not load this section.</p>;
}Tips
- Place each
<Suspense>boundary around the smallest slow section, so the rest of the page appears without waiting. - Match the size of a skeleton to the real content, so the page does not jump when
<Suspense>resolves.
Warnings
- One
<Suspense>boundary around the whole page hides everything until the slowest request finishes, which defeats the point of streaming. - With
cacheComponentsenabled, reading uncached data outside a<Suspense>boundary triggers a blocking-route error in development.
In Practice
Show product details right away and stream in slow reviews when they are ready.
- The product loads first because the heading depends on it.
- Reviews is a separate async component, so its fetch does not block the page.
- Suspense sends the fallback in the first response and swaps it later.
- The user can read and scroll the page while reviews are still loading.
// app/products/[id]/page.tsx
import { Suspense } from 'react';
import { getProduct, getReviews } from '@/lib/data';
async function Reviews({ id }: { id: string }) {
const reviews = await getReviews(id); // slow
return <p>{reviews.length} reviews</p>;
}
export default async function Page(
props: PageProps<'/products/[id]'>
) {
const { id } = await props.params;
const product = await getProduct(id);
return (
<main>
<h1>{product.name}</h1>
<Suspense fallback={<p>Loading reviews...</p>}>
<Reviews id={id} />
</Suspense>
</main>
);
}FAQ
Streaming sends a page to the browser in pieces. The layout and fast parts arrive first. Slower parts show a fallback, then appear as their data finishes loading.
loading.tsx wraps a whole route segment in one boundary automatically. lets you wrap any single component, so you control exactly which part waits.
Partial Prerendering sends a prebuilt static shell at once, then streams the dynamic parts into it. In Next.js 16 you get it by setting cacheComponents: true. The old experimental.ppr flag was removed.
No. The content is still rendered on the server and arrives in the same response. Search engines receive the complete HTML once the stream ends.