Streaming & Loading Skeletons
Without streaming, the user waits until every slow query finishes before seeing anything. With streaming, Next.js sends a shell first, then fills in slow parts as they resolve.
Why stream?
Without streaming, the user waits until every slow query finishes before seeing anything. With streaming, Next.js sends a shell first, then fills in slow parts as they resolve.
Real-life example: Streaming is a restaurant bringing water and bread while the main course cooks — you are not staring at an empty table the whole time.
Suspense boundaries
Wrap a slow async Server Component in <Suspense fallback={...}>. The fallback shows immediately; the child streams in when ready.
import { Suspense } from "react";
async function RevenueChart() {
const data = await fetch("https://api.example.com/revenue", {
cache: "no-store",
}).then((r) => r.json());
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
function ChartSkeleton() {
return <div className="h-40 animate-pulse rounded bg-zinc-200" />;
}
export default function DashboardPage() {
return (
<main>
<h1>Dashboard</h1>
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
</main>
);
}loading.tsx — route-level skeleton
A loading.tsx file next to page.tsx automatically wraps that page in Suspense. Instant navigation feels snappy because the skeleton shows while the page loads.
export default function Loading() {
return (
<div className="space-y-4 p-6">
<div className="h-8 w-48 animate-pulse rounded bg-zinc-200" />
<div className="h-32 animate-pulse rounded bg-zinc-200" />
<div className="grid gap-3 sm:grid-cols-3">
<div className="h-24 animate-pulse rounded bg-zinc-200" />
<div className="h-24 animate-pulse rounded bg-zinc-200" />
<div className="h-24 animate-pulse rounded bg-zinc-200" />
</div>
</div>
);
}