All Docs
FeaturesDepositClearUpdated March 12, 2026

PERF-14: Server Component Usage — Closing the Data-Fetching Gap

PERF-14: Server Component Usage — Closing the Data-Fetching Gap

Release: v0.1.177 · Category: Performance · Control: PERF-14

Background

Next.js App Router gives every page the opportunity to fetch data on the server and deliver a fully-populated HTML response to the browser. When that opportunity is missed, users experience a "loading waterfall": the page shell arrives, JavaScript hydrates, and then another network request goes out before any real content appears.

This post documents the PERF-14 audit findings and the recommended path to fix it.


What We Found

Across the dashboard, the data-fetching pattern looks like this:

Browser → Next.js Server
           └─ RSC renders page shell
               └─ SSR HTML delivered to browser
                   └─ JS bundle hydrates
                       └─ 'use client' component mounts
                           └─ fetch() → /api/trpc  ← real data finally arrives

Pages including DashboardPage, AnalyticsPage, and PropertiesPage are declared as React Server Components, but their server-side execution only resolves auth/session data. All meaningful data fetching is deferred to large 'use client' child components (OverviewStats, WorkQueue, PropertiesList) that issue tRPC calls after hydration.

The result is that RSC status on these pages is effectively cosmetic — the user still waits for a full client-side round-trip before seeing data.


Why It Matters

MetricCurrent patternAfter remediation
Data available at first paint❌ No✅ Yes
Network round-trips on initial load2 (SSR + tRPC)1 (SSR only)
Loading skeletons visible to userAlwaysEliminated for initial render
Interactivity preserved✅ Yes✅ Yes

For tenants and landlords landing on the dashboard after login, eliminating the waterfall means the work queue and property list are visible immediately — no skeleton screens on the critical path.


Recommended Migration Paths

Two approaches are available depending on component complexity. Both are compatible with the existing tRPC setup.

Option A: tRPC Server-Side Caller

Create a createCaller in page.tsx, execute the queries server-side, and pass results as props.

// src/app/dashboard/page.tsx
import { createCaller } from '@/server/api/root';
import { createTRPCContext } from '@/server/api/trpc';

export default async function DashboardPage() {
  const ctx = await createTRPCContext({ headers: headers() });
  const caller = createCaller(ctx);

  // Prefetch on the server — no client round-trip needed
  const [overviewStats, workQueue] = await Promise.all([
    caller.dashboard.getOverviewStats(),
    caller.dashboard.getWorkQueue(),
  ]);

  return (
    <>
      <OverviewStats initialData={overviewStats} />
      <WorkQueue initialData={workQueue} />
    </>
  );
}

Client components receive initialData and can use it directly, falling back to live tRPC queries only when the user performs an action that invalidates the cache.


Option B: HydrationBoundary Prefetching

Use trpc.prefetchQuery with React Query's HydrationBoundary to stream dehydrated query state into the client. This approach requires no prop changes to existing client components.

// src/app/dashboard/page.tsx
import { HydrationBoundary, dehydrate } from '@tanstack/react-query';
import { createServerSideHelpers } from '@trpc/react-query/server';

export default async function DashboardPage() {
  const helpers = createServerSideHelpers({ ... });

  // Prefetch — results are serialised into the HTML payload
  await helpers.dashboard.getOverviewStats.prefetch();
  await helpers.dashboard.getWorkQueue.prefetch();

  return (
    <HydrationBoundary state={dehydrate(helpers.queryClient)}>
      {/* These components find their data already in the cache */}
      <OverviewStats />
      <WorkQueue />
    </HydrationBoundary>
  );
}

This is lower friction for existing components since useQuery calls inside them will resolve immediately from the hydrated cache without any prop drilling.


Which Option Should You Use?

ScenarioRecommended approach
New page or componentOption A (caller + props) — explicit, type-safe
Existing client components with many consumersOption B (HydrationBoundary) — no prop changes needed
Components that mix server data with user interactionOption B — preserves live-query behaviour post-hydration

Affected Files

  • src/app/dashboard/page.tsx — primary remediation target
  • src/app/dashboard/analytics/page.tsx — same pattern applies
  • src/app/dashboard/properties/page.tsx — same pattern applies

Current Status

This release tracks the audit finding. No code changes have been applied yet. Follow-on work will implement Option B (HydrationBoundary) across the three highest-traffic dashboard pages as the first remediation step.