From 5f797112ec453255add5abf27723373944e1efcd Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Tue, 10 Mar 2026 00:44:19 +0100 Subject: [PATCH] Memoize expensive computations in Chart and Globe components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chart: wrap chartData (Date formatting on every data point) and metricsWithTrends in useMemo — these ran on every render. Globe: memoize marker computation (Math.max + filter + map on every render). Co-Authored-By: Claude Opus 4.6 --- components/dashboard/Chart.tsx | 8 ++++---- components/dashboard/Globe.tsx | 25 ++++++++++++++----------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/components/dashboard/Chart.tsx b/components/dashboard/Chart.tsx index bfbeb3d..89faa38 100644 --- a/components/dashboard/Chart.tsx +++ b/components/dashboard/Chart.tsx @@ -201,7 +201,7 @@ export default function Chart({ // ─── Data ────────────────────────────────────────────────────────── - const chartData = data.map((item) => { + const chartData = useMemo(() => data.map((item) => { let formattedDate: string if (interval === 'minute') { formattedDate = new Date(item.date).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }) @@ -223,7 +223,7 @@ export default function Chart({ bounce_rate: item.bounce_rate, avg_duration: item.avg_duration, } - }) + }), [data, interval]) const annotationMarkers = useMemo(() => { if (!annotations?.length) return [] @@ -298,7 +298,7 @@ export default function Chart({ // ─── Metrics with trends ────────────────────────────────────────── - const metricsWithTrends = METRIC_CONFIGS.map((m) => { + const metricsWithTrends = useMemo(() => METRIC_CONFIGS.map((m) => { const value = stats[m.key] const previousValue = prevStats?.[m.key] const change = previousValue != null && previousValue > 0 @@ -313,7 +313,7 @@ export default function Chart({ change, isPositive, } - }) + }), [stats, prevStats]) const hasData = data.length > 0 const hasAnyNonZero = hasData && chartData.some((d) => (d[metric] as number) > 0 diff --git a/components/dashboard/Globe.tsx b/components/dashboard/Globe.tsx index 173fe2a..fc7dedb 100644 --- a/components/dashboard/Globe.tsx +++ b/components/dashboard/Globe.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useRef } from 'react' +import { useEffect, useMemo, useRef } from 'react' import createGlobe from 'cobe' import { useTheme } from '@ciphera-net/ui' import { countryCentroids } from '@/lib/country-centroids' @@ -22,16 +22,19 @@ export default function Globe({ data, className }: GlobeProps) { // Update refs without causing effect re-runs isDarkRef.current = resolvedTheme === 'dark' - // Compute markers into ref - const max = data.length ? Math.max(...data.map((d) => d.pageviews)) : 0 - markersRef.current = max > 0 - ? data - .filter((d) => d.country && d.country !== 'Unknown' && countryCentroids[d.country]) - .map((d) => ({ - location: [countryCentroids[d.country].lat, countryCentroids[d.country].lng] as [number, number], - size: 0.03 + (d.pageviews / max) * 0.12, - })) - : [] + // Compute markers into ref (memoized to avoid recalculating on every render) + const markers = useMemo(() => { + const max = data.length ? Math.max(...data.map((d) => d.pageviews)) : 0 + return max > 0 + ? data + .filter((d) => d.country && d.country !== 'Unknown' && countryCentroids[d.country]) + .map((d) => ({ + location: [countryCentroids[d.country].lat, countryCentroids[d.country].lng] as [number, number], + size: 0.03 + (d.pageviews / max) * 0.12, + })) + : [] + }, [data]) + markersRef.current = markers useEffect(() => { if (!canvasRef.current) return