perf: consolidate 7 dashboard hooks into single batch request

Replace useDashboardOverview, useDashboardPages, useDashboardLocations,
useDashboardDevices, useDashboardReferrers, useDashboardPerformance, and
useDashboardGoals with a single useDashboard hook that calls the existing
/dashboard batch endpoint. This endpoint runs all queries in parallel on
the backend and caches the result in Redis (30s TTL).

Reduces dashboard requests from 12 to 6 per refresh cycle (50% reduction).
At 1,000 concurrent users: ~6,000 req/min instead of 12,000.
This commit is contained in:
Usman Baig
2026-03-10 17:55:29 +01:00
parent 00d8656ad2
commit d863004d5f
4 changed files with 56 additions and 62 deletions

View File

@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
## [Unreleased]
### Improved
- **Even faster dashboard loading.** Your dashboard now fetches all its data — pages, locations, devices, referrers, performance, and goals — in a single request instead of seven separate ones. This means the entire dashboard appears at once rather than sections loading one by one, and puts much less strain on the server when many people are viewing their analytics at the same time.
### Added
- **Interactive 3D Globe.** The Locations panel now has a "Globe" tab showing your visitor locations on a beautiful, interactive 3D globe. Drag to rotate, and orange markers highlight where your visitors are — sized by how much traffic each country sends. The globe slowly auto-rotates and adapts to light and dark mode.

View File

@@ -40,13 +40,7 @@ const EventProperties = dynamic(() => import('@/components/dashboard/EventProper
const ExportModal = dynamic(() => import('@/components/dashboard/ExportModal'))
import { type DimensionFilter, serializeFilters, parseFiltersFromURL } from '@/lib/filters'
import {
useDashboardOverview,
useDashboardPages,
useDashboardLocations,
useDashboardDevices,
useDashboardReferrers,
useDashboardPerformance,
useDashboardGoals,
useDashboard,
useRealtime,
useStats,
useDailyStats,
@@ -220,16 +214,10 @@ export default function SiteDashboardPage() {
return { start: prevStart.toISOString().split('T')[0], end: prevEnd.toISOString().split('T')[0] }
}, [dateRange])
// SWR hooks - replace manual useState + useEffect + setInterval polling
// Each hook handles its own refresh interval, deduplication, and error retry
// Filters are included in cache keys so changing filters auto-refetches
const { data: overview, isLoading: overviewLoading, error: overviewError } = useDashboardOverview(siteId, dateRange.start, dateRange.end, interval, filtersParam || undefined)
const { data: pages } = useDashboardPages(siteId, dateRange.start, dateRange.end, filtersParam || undefined)
const { data: locations } = useDashboardLocations(siteId, dateRange.start, dateRange.end, filtersParam || undefined)
const { data: devicesData } = useDashboardDevices(siteId, dateRange.start, dateRange.end, filtersParam || undefined)
const { data: referrers } = useDashboardReferrers(siteId, dateRange.start, dateRange.end, filtersParam || undefined)
const { data: performanceData } = useDashboardPerformance(siteId, dateRange.start, dateRange.end, filtersParam || undefined)
const { data: goalsData } = useDashboardGoals(siteId, dateRange.start, dateRange.end, filtersParam || undefined)
// Single dashboard request replaces 7 focused hooks (overview, pages, locations,
// devices, referrers, performance, goals). The backend runs all queries in parallel
// and caches the result in Redis, reducing requests from 12 to 6 per refresh cycle.
const { data: dashboard, isLoading: dashboardLoading, error: dashboardError } = useDashboard(siteId, dateRange.start, dateRange.end, interval, filtersParam || undefined)
const { data: realtimeData } = useRealtime(siteId)
const { data: prevStats } = useStats(siteId, prevRange.start, prevRange.end)
const { data: prevDailyStats } = useDailyStats(siteId, prevRange.start, prevRange.end, interval)
@@ -255,24 +243,24 @@ export default function SiteDashboardPage() {
toast.success('Annotation deleted')
}
// Derive typed values from SWR data
const site = overview?.site ?? null
const stats: Stats = overview?.stats ?? { pageviews: 0, visitors: 0, bounce_rate: 0, avg_duration: 0 }
const realtime = realtimeData?.visitors ?? overview?.realtime_visitors ?? 0
const dailyStats: DailyStat[] = overview?.daily_stats ?? []
// Derive typed values from single dashboard response
const site = dashboard?.site ?? null
const stats: Stats = dashboard?.stats ?? { pageviews: 0, visitors: 0, bounce_rate: 0, avg_duration: 0 }
const realtime = realtimeData?.visitors ?? dashboard?.realtime_visitors ?? 0
const dailyStats: DailyStat[] = dashboard?.daily_stats ?? []
// Build filter suggestions from current dashboard data
const filterSuggestions = useMemo<FilterSuggestions>(() => {
const s: FilterSuggestions = {}
// Pages
const topPages = pages?.top_pages ?? []
const topPages = dashboard?.top_pages ?? []
if (topPages.length > 0) {
s.page = topPages.map(p => ({ value: p.path, label: p.path, count: p.pageviews }))
}
// Referrers
const refs = referrers?.top_referrers ?? []
const refs = dashboard?.top_referrers ?? []
if (refs.length > 0) {
s.referrer = refs.filter(r => r.referrer && r.referrer !== '').map(r => ({
value: r.referrer,
@@ -282,7 +270,7 @@ export default function SiteDashboardPage() {
}
// Countries
const ctrs = locations?.countries ?? []
const ctrs = dashboard?.countries ?? []
if (ctrs.length > 0) {
const regionNames = (() => { try { return new Intl.DisplayNames(['en'], { type: 'region' }) } catch { return null } })()
s.country = ctrs.filter(c => c.country && c.country !== 'Unknown').map(c => ({
@@ -293,7 +281,7 @@ export default function SiteDashboardPage() {
}
// Regions
const regs = locations?.regions ?? []
const regs = dashboard?.regions ?? []
if (regs.length > 0) {
s.region = regs.filter(r => r.region && r.region !== 'Unknown').map(r => ({
value: r.region,
@@ -303,7 +291,7 @@ export default function SiteDashboardPage() {
}
// Cities
const cts = locations?.cities ?? []
const cts = dashboard?.cities ?? []
if (cts.length > 0) {
s.city = cts.filter(c => c.city && c.city !== 'Unknown').map(c => ({
value: c.city,
@@ -313,7 +301,7 @@ export default function SiteDashboardPage() {
}
// Browsers
const brs = devicesData?.browsers ?? []
const brs = dashboard?.browsers ?? []
if (brs.length > 0) {
s.browser = brs.filter(b => b.browser && b.browser !== 'Unknown').map(b => ({
value: b.browser,
@@ -323,7 +311,7 @@ export default function SiteDashboardPage() {
}
// OS
const oses = devicesData?.os ?? []
const oses = dashboard?.os ?? []
if (oses.length > 0) {
s.os = oses.filter(o => o.os && o.os !== 'Unknown').map(o => ({
value: o.os,
@@ -333,7 +321,7 @@ export default function SiteDashboardPage() {
}
// Devices
const devs = devicesData?.devices ?? []
const devs = dashboard?.devices ?? []
if (devs.length > 0) {
s.device = devs.filter(d => d.device && d.device !== 'Unknown').map(d => ({
value: d.device,
@@ -359,19 +347,19 @@ export default function SiteDashboardPage() {
}
return s
}, [pages, referrers, locations, devicesData, campaigns])
}, [dashboard, campaigns])
// Show error toast on fetch failure
useEffect(() => {
if (overviewError) {
if (dashboardError) {
toast.error('Failed to load dashboard analytics')
}
}, [overviewError])
}, [dashboardError])
// Track when data was last updated (for "Live · Xs ago" display)
useEffect(() => {
if (overview) lastUpdatedAtRef.current = Date.now()
}, [overview])
if (dashboard) lastUpdatedAtRef.current = Date.now()
}, [dashboard])
// Save settings to localStorage
const saveSettings = (type: string, newDateRange?: { start: string; end: string }) => {
@@ -413,7 +401,7 @@ export default function SiteDashboardPage() {
// Skip the minimum-loading skeleton when SWR already has cached data
// (prevents the 300ms flash when navigating back to the dashboard)
const showSkeleton = useMinimumLoading(overviewLoading && !overview)
const showSkeleton = useMinimumLoading(dashboardLoading && !dashboard)
if (showSkeleton) {
return <DashboardSkeleton />
@@ -543,8 +531,8 @@ export default function SiteDashboardPage() {
{site.enable_performance_insights && (
<div className="mb-8">
<PerformanceStats
stats={performanceData?.performance ?? { lcp: 0, cls: 0, inp: 0 }}
performanceByPage={performanceData?.performance_by_page ?? null}
stats={dashboard?.performance ?? { lcp: 0, cls: 0, inp: 0 }}
performanceByPage={dashboard?.performance_by_page ?? null}
siteId={siteId}
startDate={dateRange.start}
endDate={dateRange.end}
@@ -555,9 +543,9 @@ export default function SiteDashboardPage() {
<div className="grid gap-6 lg:grid-cols-2 mb-8">
<ContentStats
topPages={pages?.top_pages ?? []}
entryPages={pages?.entry_pages ?? []}
exitPages={pages?.exit_pages ?? []}
topPages={dashboard?.top_pages ?? []}
entryPages={dashboard?.entry_pages ?? []}
exitPages={dashboard?.exit_pages ?? []}
domain={site.domain}
collectPagePaths={site.collect_page_paths ?? true}
siteId={siteId}
@@ -565,7 +553,7 @@ export default function SiteDashboardPage() {
onFilter={handleAddFilter}
/>
<TopReferrers
referrers={referrers?.top_referrers ?? []}
referrers={dashboard?.top_referrers ?? []}
collectReferrers={site.collect_referrers ?? true}
siteId={siteId}
dateRange={dateRange}
@@ -575,19 +563,19 @@ export default function SiteDashboardPage() {
<div className="grid gap-6 lg:grid-cols-2 mb-8">
<Locations
countries={locations?.countries ?? []}
cities={locations?.cities ?? []}
regions={locations?.regions ?? []}
countries={dashboard?.countries ?? []}
cities={dashboard?.cities ?? []}
regions={dashboard?.regions ?? []}
geoDataLevel={site.collect_geo_data || 'full'}
siteId={siteId}
dateRange={dateRange}
onFilter={handleAddFilter}
/>
<TechSpecs
browsers={devicesData?.browsers ?? []}
os={devicesData?.os ?? []}
devices={devicesData?.devices ?? []}
screenResolutions={devicesData?.screen_resolutions ?? []}
browsers={dashboard?.browsers ?? []}
os={dashboard?.os ?? []}
devices={dashboard?.devices ?? []}
screenResolutions={dashboard?.screen_resolutions ?? []}
collectDeviceInfo={site.collect_device_info ?? true}
collectScreenResolution={site.collect_screen_resolution ?? true}
siteId={siteId}
@@ -599,13 +587,13 @@ export default function SiteDashboardPage() {
<div className="grid gap-6 lg:grid-cols-2 mb-8">
<Campaigns siteId={siteId} dateRange={dateRange} filters={filtersParam || undefined} onFilter={handleAddFilter} />
<GoalStats
goalCounts={(goalsData?.goal_counts ?? []).filter(g => !/^scroll_\d+$/.test(g.event_name))}
goalCounts={(dashboard?.goal_counts ?? []).filter(g => !/^scroll_\d+$/.test(g.event_name))}
onSelectEvent={setSelectedEvent}
/>
</div>
<div className="mb-8">
<ScrollDepth goalCounts={goalsData?.goal_counts ?? []} totalPageviews={stats.pageviews} />
<ScrollDepth goalCounts={dashboard?.goal_counts ?? []} totalPageviews={stats.pageviews} />
</div>
{/* Event Properties Breakdown */}
@@ -636,8 +624,8 @@ export default function SiteDashboardPage() {
onClose={() => setIsExportModalOpen(false)}
data={dailyStats}
stats={stats}
topPages={pages?.top_pages}
topReferrers={referrers?.top_referrers}
topPages={dashboard?.top_pages}
topReferrers={dashboard?.top_referrers}
campaigns={campaigns}
/>
</div>

View File

@@ -245,8 +245,8 @@ export interface DashboardData {
goal_counts?: GoalCountStat[]
}
export function getDashboard(siteId: string, startDate?: string, endDate?: string, limit = 10, interval?: string): Promise<DashboardData> {
return apiRequest<DashboardData>(`/sites/${siteId}/dashboard${buildQuery({ startDate, endDate, limit, interval })}`)
export function getDashboard(siteId: string, startDate?: string, endDate?: string, limit = 10, interval?: string, filters?: string): Promise<DashboardData> {
return apiRequest<DashboardData>(`/sites/${siteId}/dashboard${buildQuery({ startDate, endDate, limit, interval, filters })}`)
}
export function getPublicDashboard(

View File

@@ -24,6 +24,7 @@ import type {
Stats,
DailyStat,
CampaignStat,
DashboardData,
DashboardOverviewData,
DashboardPagesData,
DashboardLocationsData,
@@ -36,7 +37,7 @@ import type {
// * SWR fetcher functions
const fetchers = {
site: (siteId: string) => getSite(siteId),
dashboard: (siteId: string, start: string, end: string) => getDashboard(siteId, start, end),
dashboard: (siteId: string, start: string, end: string, interval?: string, filters?: string) => getDashboard(siteId, start, end, 10, interval, filters),
dashboardOverview: (siteId: string, start: string, end: string, interval?: string, filters?: string) => getDashboardOverview(siteId, start, end, interval, filters),
dashboardPages: (siteId: string, start: string, end: string, filters?: string) => getDashboardPages(siteId, start, end, undefined, filters),
dashboardLocations: (siteId: string, start: string, end: string, filters?: string) => getDashboardLocations(siteId, start, end, undefined, undefined, filters),
@@ -81,14 +82,15 @@ export function useSite(siteId: string) {
)
}
// * Hook for dashboard summary data (refreshed less frequently)
export function useDashboard(siteId: string, start: string, end: string) {
return useSWR(
siteId && start && end ? ['dashboard', siteId, start, end] : null,
() => fetchers.dashboard(siteId, start, end),
// * Hook for full dashboard data (single request replaces 7 focused hooks)
// * The backend runs all queries in parallel and caches the result in Redis (30s TTL)
export function useDashboard(siteId: string, start: string, end: string, interval?: string, filters?: string) {
return useSWR<DashboardData>(
siteId && start && end ? ['dashboard', siteId, start, end, interval, filters] : null,
() => fetchers.dashboard(siteId, start, end, interval, filters),
{
...dashboardSWRConfig,
// * Refresh every 60 seconds for dashboard summary
// * Refresh every 60 seconds for dashboard data
refreshInterval: 60 * 1000,
// * Deduping interval to prevent duplicate requests
dedupingInterval: 10 * 1000,