All Docs
FeaturesMaking Tax DigitalUpdated March 9, 2026

Performance Deep-Dive: Making Server Components Actually Work for You (PERF-14)

Performance Deep-Dive: Making Server Components Actually Work for You (PERF-14)

Release: v1.0.336 · Performance Audit · PERF-14

Background

Next.js App Router pages are Server Components by default. For a Making Tax Digital platform where users need instant visibility of their HMRC connection status, quarterly submission deadlines, and income summaries, the RSC layer is a natural place to front-load data fetching — removing latency from the user's critical path.

Our PERF-14 audit identified that this opportunity is currently not being taken on the main dashboard route.

What We Found

src/app/dashboard/page.tsx is a Server Component, but in practice it only does two things:

  1. Checks that the user is authenticated.
  2. Passes a small number of static props down to child components.

All real data fetching — dashboard stats, HMRC connection status, submission history — happens inside 'use client' components via tRPC queries that fire after client-side hydration.

The Waterfall

Browser request
    │
    ▼
Server renders shell HTML  ← no data fetched here
    │
    ▼
Browser downloads JS bundle
    │
    ▼
Client components mount
    │
    ▼
tRPC queries fire           ← data fetching starts here
    │
    ▼
UI renders with data

For users on average connections this adds a perceptible loading state before any above-the-fold content appears — even though the page is ostensibly server-rendered.

Why This Matters for MTD Users

The dashboard is the primary entry point for landlords and self-employed taxpayers checking:

  • Whether their HMRC OAuth connection is still active
  • Upcoming quarterly submission deadlines
  • Current period income and expense totals
  • Status of in-progress or recently submitted returns

All of this is high-urgency, above-the-fold content. Delaying it by a full client-hydration round-trip means users frequently see spinners or skeleton screens on every page load, even when the underlying data hasn't changed.

The Fix

Two implementation patterns can resolve this, both compatible with the existing 'use client' component architecture.

Option A — Props as initialData

Fetch critical data inside page.tsx using the tRPC server-side caller and pass results directly as initialData to client components.

// src/app/dashboard/page.tsx (simplified illustration)
import { createCaller } from '~/server/api/root';
import { createTRPCContext } from '~/server/api/trpc';
import { DashboardStats } from './_components/DashboardStats';

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

  const [stats, hmrcStatus] = await Promise.all([
    caller.dashboard.getStats(),
    caller.hmrc.getConnectionStatus(),
  ]);

  return (
    <DashboardStats
      initialStats={stats}
      initialHmrcStatus={hmrcStatus}
    />
  );
}

Client components receive initialData and render immediately on mount — no loading state for content that was already available at request time.

Option B — HydrationBoundary

Use tRPC's server-side caller to pre-populate the React Query cache, then wrap client components in <HydrationBoundary>. Client components call their usual useQuery hooks but find the cache already warm.

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

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

  await Promise.all([
    helpers.dashboard.getStats.prefetch(),
    helpers.hmrc.getConnectionStatus.prefetch(),
  ]);

  return (
    <HydrationBoundary state={dehydrate(helpers.queryClient)}>
      <DashboardShell />
    </HydrationBoundary>
  );
}

This approach requires no changes to existing client components — they simply find their queries pre-populated on first render.

What Stays the Same

  • Client components remain 'use client' — no migration required.
  • Subsequent navigations and background refetches continue to use tRPC normally.
  • Authentication checks remain in the Server Component layer.
  • Non-critical or below-the-fold data can continue to fetch client-side.

Affected File

FileChange Needed
src/app/dashboard/page.tsxAdd server-side prefetch for dashboard stats and HMRC connection status

Status

This is a tracked performance control (PERF-14). The finding has been recorded and the recommended fix is queued for implementation. No functional behaviour changes in this release.