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

View File

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

View File

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