Performance & Caching

CosmosCost layers render-time memoization, API caches, and lightweight client state updates to keep the dashboard responsive while protecting Supabase from expensive fan-out queries.

Server-Side Caching Layers

The dashboard page combines three caching strategies before any client code runs:

  • Render-time deduplication getDashboardOverviewSnapshot in src/lib/dashboard/get-dashboard-overview.ts is wrapped in cache(), so identical argument sets only execute once per request even when multiple server components demand the same data.
  • Incremental revalidation src/app/dashboard/[slug]/page.tsx exports revalidate = 300, caching the rendered HTML for 5 minutes before Next.js schedules a background refresh.
  • API caches for follow-up fetches – expensive JSON payloads are guarded with unstable_cache so client re-fetches hit warm data instead of re-running Supabase RPC chains.
ScopeLocationTTLCache Controls
Dashboard SSR rendersrc/app/dashboard/[slug]/page.tsx5 minutesrevalidate = 300
Dashboard snapshot APIsrc/app/api/customers/[slug]/dashboard/snapshot/route.ts5 minutesunstable_cache(..., { revalidate: 300, tags: ['dashboard'] }), s-maxage=300
Daily cost trends APIsrc/app/api/customers/[slug]/costs/daily/route.ts1 hourunstable_cache(..., { revalidate: 3600, tags: ['costs'] }), s-maxage=3600
Admin analytics helperssrc/lib/api/admin-cache.ts5–60 minutesunstable_cache with tag families such as 'admin:users'

The snapshot endpoint takes a Supabase client from the request context before entering the cached callback to avoid cookie lookups inside the cache scope, then returns both the overview data and subscription snapshot using Promise.all. The cost trends API streams large result sets through processCostDataStream to prevent long event loops and attaches stale-while-revalidate headers so Vercel Edge can serve cached payloads while regeneration runs.

Client State & Preference Persistence

Optimistic UI keeps interactions instant while Supabase writes occur in the background:

  • src/components/dashboard/DashboardClient.tsx maintains the selected quick metrics and cost trend visualizations. persistQuickMetricSelection and persistTrendSelection update React state immediately, trigger PATCH requests, and roll back if the network responds with an error.
  • Time period buttons call router.push with updated ?days= values so the server component re-renders with fresh data while preserving layout state.
  • DashboardLayout toggles sections by updating activeSection and using window.history.pushState, avoiding a full navigation. When the user returns to Overview, the original DashboardClient instance renders, so state survives round-trips through Analytics or Reports.
  • SubscriptionProvider wraps the overview, analytics, reports, and billing sections. It receives initialSubscription from the server render, exposes the normalized plan snapshot to child components, and refreshes in the background only when a customer ID is present.
  • The legacy DashboardOverview component still hydrates historical props with useRef guards (for example quickMetricsHydratedRef) to suppress redundant API calls during the migration period.

User Preference Loading

User selections are stored per customer in Supabase:

  • ensureQuickMetricPreferences and ensureCostTrendPreferences in src/lib/dashboard/preferences.ts query the user_preferences table, seed defaults when no row exists, and sanitize IDs before returning.
  • getDashboardOverviewSnapshot calls those helpers inside the main Promise.all alongside the cost aggregates, meaning quick metrics, cost trend IDs, activity history, and totals arrive as a single object that seeds the client.
  • A cached batch helper (ensureBatchDashboardPreferences) exists for RPC-based aggregation, but the current implementation relies on the explicit helpers above for clarity and easier error handling.

Cache Invalidation & Tagging

Supabase functions power cache-aware workflows so long-lived caches never drift:

  • src/lib/dashboard/cache-revalidation.ts defines reusable tag generators such as dashboard(customerId), providers(customerId), and providerCosts(customerId, provider).
  • Helper utilities (invalidateDashboardMetrics, invalidateProviderCaches, invalidateServiceCaches, and invalidateCustomerCache) call Supabase RPCs to coordinate cache clears after imports or account changes.
  • Admin dashboards can refresh materialized views through refreshCostAggregationViews, which wraps the refresh_cost_aggregation_views RPC and is safe to run inside background jobs.

Database Optimizations

Aggregations are handled directly in SQL to minimize application CPU time:

  • fetchCostsByDate, fetchCostsByProvider, and fetchCostsByService first attempt the _fast materialized view RPCs and fall back to the baseline functions if the optimized views have not been refreshed yet.
  • fetchCostSummary aggregates total spend, providers, services, and record counts in a single RPC call so the application only performs lightweight number parsing.
  • The daily costs API chunks large result sets through processCostDataStream, yielding the event loop every 500 records to avoid blocking concurrent requests.

Key Performance Improvements

  • src/app/dashboard/[slug]/loading.tsx renders DashboardPageSkeleton, providing instant visual feedback during the initial server render and keeping perceived load time low.
  • DashboardLayout lazily loads heavy client bundles such as analytics, billing, and cloud accounts pages with next/dynamic, while CostTrendSection defers chart rendering until Recharts is loaded.
  • Chart calculations and headline metrics rely on memoized selectors inside DashboardClient, ensuring rerenders are cheap even when new data arrives.

End-to-End Request Flow

  1. User visits /dashboard/[slug]?days=30. Next.js reuses the cached HTML if the 5 minute window is still valid; otherwise the page is regenerated.
  2. getDashboardOverviewSnapshot runs (once per argument set), executing the cost RPCs, user preference helpers, and activity fetch in parallel.
  3. The resulting snapshot plus getSubscriptionSnapshot feed DashboardClient via initialContent. The layout keeps that client instance alive while the user navigates to other sections.
  4. Subsequent client requests hit the cached API routes. Cache headers allow Edge nodes to serve the last response while background regeneration runs.
  5. Admin or ingestion workflows call cache invalidation helpers so future requests pull new data without waiting for TTL expiry.

Operational Tips

  • Inspect cache headers in the browser Network tab (s-maxage and stale-while-revalidate) to confirm which layer handled the response.
  • Use get_cache_status via cache-revalidation.ts when diagnosing stale dashboards; it reports which tag groups still require refresh.
  • When adjusting Supabase schemas or materialized views, refresh them with refreshCostAggregationViews and then invalidate the relevant tags to prevent mixed schema versions.