Fixing the Silent Performance Drain: React Query staleTime
Fixing the Silent Performance Drain: React Query staleTime
Release: v1.0.16 · Category: Caching · File:
src/lib/trpc/provider.tsx
The Problem
Since the platform's tRPC layer was first wired up, QueryClient has been initialised with no configuration at all:
// Before — zero configuration
new QueryClient()
This is the default recommended in many tutorials, and it works fine for small apps. At the scale of the SaaS Factory dashboard — which issues 15+ tRPC queries per page — it becomes a serious performance liability.
Here's why: React Query's default staleTime is 0. That means the moment a query result lands in the cache it is considered stale. The next time any component that subscribes to that query mounts (including during client-side navigation between pages), React Query fires a fresh network request to revalidate it — even if the data is half a second old.
The result is a waterfall of API calls on every navigation event, visible as a burst of network activity in DevTools and felt as a sluggish UI.
The Fix
The solution is a two-line change in the QueryClient constructor:
// After — explicit cache defaults
new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000, // data is fresh for 30 seconds
gcTime: 300_000, // unused cache entries evicted after 5 minutes
},
},
})
| Setting | Value | Effect |
|---|---|---|
staleTime | 30 000 ms | Cached results are treated as fresh for 30 s; no background refetch on mount during that window |
gcTime | 300 000 ms | Cache entries that have no active subscribers are garbage-collected after 5 min, keeping memory tidy |
What Stays the Same
This change only affects queries that rely on the global default. Queries that already declare their own refetchInterval are completely unaffected:
- Revenue analytics & cohort retention — poll every 5 minutes via explicit
refetchInterval. No change needed or applied. - Beast-mode status — polls every 2 seconds via explicit
refetchInterval. Continues to do so without interruption.
Impact
- ~60–70% reduction in redundant API calls during normal dashboard navigation
- Measurable improvement in UI responsiveness, particularly noticeable when moving between data-heavy pages
- No change to data freshness for any query that manages its own polling cadence
Guidance for New Queries
When adding new tRPC queries to the platform, follow these conventions:
- Rely on the global default (
staleTime: 30_000) for most read queries — user data, feature lists, pipeline state, etc. - Set
refetchIntervalexplicitly for queries that need live data (e.g. job status, real-time metrics). The globalstaleTimedoes not interfere with interval-based polling. - Set
staleTime: Infinityon the individual query if the data is truly static (e.g. app configuration loaded once at startup).
// Example: static config — never refetch in the background
api.config.getAppSettings.useQuery(undefined, {
staleTime: Infinity,
});
// Example: live job status — poll every 2 s
api.beastMode.getStatus.useQuery({ jobId }, {
refetchInterval: 2_000,
});
// Example: standard dashboard query — inherits global 30 s staleTime
api.revenue.getSummary.useQuery({ period: '30d' });