All Docs
FeaturesCalmony Sanctions MonitorUpdated March 12, 2026

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

RouteChange
/dashboard/peopleShell → Server Component; interactive elements → PeopleListClient
/dashboard/matchesIdentified for same conversion under PERF-14
/dashboard/billingIdentified 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.