after using Suspense for RevenueChart and LatestInvoices, the only part left that uses the page wide Suspense is the Cards. If there is only one part left, is there no point in separating it by its own granular suspense?
Examined granular Suspense necessity and design trade-offs
Examined granular Suspense necessity and design trade-offs
Check where the await actually lives — I suspect that's the real thing biting you.
In the tutorial, page.tsx looks like this:
tsx
export default async function Page() {
const cardData = await fetchCardData(); // ← page itself suspends here
return (
<main>
<h1>Dashboard</h1>
<Card value={cardData.numberOfInvoices} ... />
<Suspense fallback={<RevenueChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<LatestInvoicesSkeleton />}>
<LatestInvoices />
</Suspense>
</main>
);
}
Those two <Suspense> elements can't do anything yet. They only exist once Page returns — and Page can't return until fetchCardData() resolves. So the nearest boundary that's actually live during that wait is the one loading.tsx installed above the whole page. You get the full-page skeleton, then the heading and both fallbacks appear together. Your granular boundaries are functionally dead weight.
So it's not "one thing left" — it's "one thing left that is still gating everything else." Move the await down:
tsx
export default async function Page() { // no await
return (
<main>
<h1>Dashboard</h1>
<Suspense fallback={<CardsSkeleton />}>
<CardWrapper /> {/* await moved in here */}
</Suspense>
...
</main>
);
}
Now Page returns synchronously, the shell flushes immediately — heading plus three fallbacks — and each boundary fills in as its data lands.