PERF-18: Caching Strategy — Current State & Recommendations
PERF-18: Caching Strategy — Current State & Recommendations
Release: v1.0.80
Category: Performance · Data Patterns
Status: Audit complete — remediation pending
Overview
As part of a systematic performance audit of the Irish block management platform, PERF-18 examined the caching posture across the full data stack — from the tRPC QueryClient configuration on the client, through to server-side data fetching in Next.js.
The audit found that no caching layer is in place at any level. This means every user interaction that triggers a data fetch results in a full round-trip to the database, regardless of how expensive the underlying query is or how recently the same data was fetched.
Current State
Client-side: React Query / tRPC
The tRPC provider (src/lib/trpc/provider.tsx) initialises a QueryClient with default React Query settings. The critical implication is that the default staleTime is 0 ms — every cached query is considered stale immediately after it resolves. As a result, React Query refetches data on every component mount, even if the same query ran moments earlier.
// Current behaviour — staleTime defaults to 0ms
const queryClient = new QueryClient();
Server-side: No unstable_cache or React cache()
Neither of Next.js's server-side caching primitives is used anywhere in the codebase:
| Primitive | Usage |
|---|---|
unstable_cache (Next.js) | ❌ Not used |
cache() (React) | ❌ Not used |
This means every server component render and every server action triggers a fresh database query.
High-Impact Queries
Two report queries are particularly expensive and are currently uncached:
| Query | Cost |
|---|---|
agedDebtorsReport | Joins 4 tables; groups results into 5 age buckets |
arrearsReport | Complex aggregation across service charge and payment records |
Both are fetched fresh on every visit to the relevant report pages.
Reference Data
The Irish legislation reference data router is approximately 66 KB in size. This file is parsed and evaluated on every request with no memoisation, adding unnecessary overhead to any route that depends on it.
Why This Matters
For an OMC (Owners' Management Company) management platform, reports like aged debtors and arrears are frequently consulted by agents — sometimes multiple times per session. Without caching:
- Database load scales linearly with page views, even for identical requests.
- Report pages feel slow because every visit incurs full aggregation query costs.
- Reference data (legislation lookups, etc.) is redundantly re-fetched despite being essentially static.
Recommendations
1. Set a global staleTime in QueryClient
Updating the QueryClient initialisation to use a sensible default staleTime will immediately reduce redundant client-side refetches for the vast majority of queries.
// src/lib/trpc/provider.tsx
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000, // 30 seconds
},
},
});
This means data fetched within the last 30 seconds will be served from the React Query cache rather than triggering a new network request. Individual queries can override this default where fresher data is required.
2. Cache expensive aggregate reports with unstable_cache
For agedDebtorsReport and arrearsReport, wrap the underlying data fetcher in Next.js unstable_cache with a short revalidation window and a cache tag for targeted invalidation (e.g. when a payment is recorded).
import { unstable_cache } from 'next/cache';
const getCachedArrearsReport = unstable_cache(
fetchArrearsData,
['arrears-report'],
{
revalidate: 300, // 5 minutes
tags: ['arrears'],
}
);
Using cache tags means that when a payment or adjustment is posted, you can call revalidateTag('arrears') to immediately invalidate stale report data without waiting for the TTL to expire.
3. Deduplicate within-request auth lookups with React cache()
Server components often call auth helper functions multiple times within a single render tree. React's cache() function deduplicates these calls so the underlying lookup runs only once per request.
import { cache } from 'react';
export const getCurrentUser = cache(async () => {
// Auth lookup — runs at most once per server render
return await getSessionUser();
});
This is particularly valuable for auth checks that are repeated across layout, page, and nested server components in the same route.
Affected File
src/lib/trpc/provider.tsx—QueryClientinitialisation with default (0 ms)staleTime
Next Steps
The three recommendations above are tracked for implementation in upcoming releases. Priority order:
staleTimedefault (lowest risk, broadest impact)unstable_cacheonagedDebtorsReportandarrearsReportReact cache()on auth helpers