Performance Issue: Missing Suspense Boundaries on Dashboard Routes
Performance Issue: Missing Suspense Boundaries on Dashboard Routes
Control: PERF-07
Version introduced: v0.1.24
Category: Frontend Performance
Overview
A performance gap has been identified in the dashboard: no loading.tsx files or React Suspense boundaries are present in any dashboard route segment. This means the Next.js App Router cannot stream partial responses or show progressive UI while server-side data fetching is in progress. Users experience a blank white screen on every RSC page load.
Why This Matters
Next.js App Router supports streaming and progressive rendering via React Suspense. When a loading.tsx file is present in a route segment, Next.js automatically:
- Wraps the page in a
<Suspense>boundary. - Immediately sends the loading UI to the client (no white screen).
- Streams the fully rendered page content once data fetching is complete.
Without these files, the server holds the entire response until all data is resolved, leaving the browser with nothing to render in the meantime.
Affected Routes
| Route | File Missing |
|---|---|
/dashboard | src/app/dashboard/loading.tsx |
/dashboard/people | src/app/dashboard/people/loading.tsx |
/dashboard/matches | src/app/dashboard/matches/loading.tsx |
| Other nested segments | Respective loading.tsx |
Recommended Resolution
Option A — Spinner (minimal)
Add a loading.tsx to each route segment using the existing Loader2 component:
// src/app/dashboard/loading.tsx
import { Loader2 } from 'lucide-react';
export default function Loading() {
return (
<div className="flex h-full w-full items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
Option B — Skeleton cards (recommended)
Build skeleton cards that mirror the stat card layout used on the dashboard. This provides a more polished experience and prevents layout shift when content loads.
// src/app/dashboard/loading.tsx
import { Skeleton } from '@/components/ui/skeleton';
export default function Loading() {
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="rounded-lg border p-6">
<Skeleton className="h-4 w-24 mb-4" />
<Skeleton className="h-8 w-16" />
</div>
))}
</div>
);
}
Wrapping Async Components Manually
For fine-grained streaming within a single page, you can also wrap individual async Server Components in <Suspense> directly:
import { Suspense } from 'react';
import { Loader2 } from 'lucide-react';
import { SanctionsTable } from '@/components/sanctions-table';
export default function PeoplePage() {
return (
<Suspense fallback={<Loader2 className="h-6 w-6 animate-spin" />}>
<SanctionsTable />
</Suspense>
);
}