Faster Dashboards with React Server Components
Faster Dashboards with React Server Components
Release: v0.1.84 · Track: PERF-14
Background
The compliance dashboard surfaces large lists of screened people, sanctions matches, and billing records. Before this release, each of these pages was a monolithic 'use client' component: the browser downloaded the full component bundle, mounted it, and only then fired a useEffect to fetch the data it needed to display. The result was a flash of empty content — a loading spinner — on every navigation.
This is a well-understood React anti-pattern. When a page has no interactive state at initial render, there is no reason to push data-fetching to the browser.
What was changed
Page shells → Server Components
The heavy dashboard pages are converted to React Server Components (RSC). The page function becomes async, fetches data directly (on the server), and returns fully-populated JSX. Because the work happens on the server, the browser receives pre-rendered HTML with real data — no second round-trip needed.
// Before (simplified)
'use client';
export default function PeoplePage() {
const [people, setPeople] = useState([]);
useEffect(() => {
fetch('/api/people').then(r => r.json()).then(setPeople);
}, []);
return <PeopleList people={people} />; // renders empty on first paint
}
// After (simplified)
// No 'use client' directive — this is a Server Component
export default async function PeoplePage() {
const people = await fetchPeople(); // runs on the server
return <PeopleListClient initialPeople={people} />;
}
Interactive elements → PeopleListClient
Elements that genuinely require client-side state — the search bar, filter selects, and pagination controls — are extracted into a dedicated PeopleListClient client component. This component receives the server-fetched data as props and takes over from there.
This boundary means:
- Only the interactive slice of the page ships as JavaScript to the browser.
- The static frame (page header, table structure, initial rows) is never in the client bundle.
Pages affected
| Route | Change |
|---|---|
/dashboard/people | Shell → Server Component; interactive elements → PeopleListClient |
/dashboard/matches | Identified for same conversion under PERF-14 |
/dashboard/billing | Identified for same conversion under PERF-14 |
User-visible improvements
- No more loading spinner on initial navigation to the People, Matches, or Billing pages.
- Faster Time to First Contentful Paint (FCP) — the server sends populated HTML rather than an empty shell.
- Reduced JavaScript bundle size — large page components that were previously in the client bundle are now server-only code.
- Improved Core Web Vitals — particularly LCP (Largest Contentful Paint), since the primary content block is present in the initial HTML response.
Architecture note
This change follows the recommended Next.js App Router pattern for data-heavy pages:
Page (Server Component)
└── Async data fetch (server-side)
└── <InteractiveClient initialData={data} /> ← 'use client'
└── useState / useEffect for local interactivity
└── Search, filters, pagination
The Server Component acts as a data-fetching shell. The client component is narrow and purpose-built, keeping the client bundle as small as possible.
No breaking changes
This is a transparent performance optimisation. All routes, visible behaviours, and data remain identical. No changes are required from users or integrators.