From 985949b7b000ac2bf39237ffe95e7a43947ba9bc Mon Sep 17 00:00:00 2001 From: bokiko Date: Thu, 19 Mar 2026 18:20:07 +0000 Subject: [PATCH] perf: memoize dashboard derived state to eliminate redundant renders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All derived values on the dashboard (filteredResults, avgPing, avgPacketLoss, avgJitter, regions, chartData, serverChartData) were recomputed inline on every React render — including renders triggered by unrelated state updates like loading toggling back to false. Each computation is now wrapped in useMemo with the tightest possible dependency array: - filteredResults: depends on results, applyDateFilter, selectedRegion - avgPing / avgPacketLoss / avgJitter: depend on filteredResults only - regions: depends on results only (not filter state) - chartData: depends on filteredResults - serverChartData: depends on filteredResults For a typical result set (~100 items) this eliminates 5 O(n) reduce passes and 2 groupBy passes on every render that doesn't change the underlying data. At 100 results the savings are modest; at the 500–1000 result scale the dashboard would hit without this change the difference is measurable. Memoization also makes the data flow explicit — it's now clear at a glance which data depends on which filters. --- web/src/app/dashboard/page.tsx | 132 +++++++++++++++++++-------------- 1 file changed, 76 insertions(+), 56 deletions(-) diff --git a/web/src/app/dashboard/page.tsx b/web/src/app/dashboard/page.tsx index 137c80e..dcac414 100644 --- a/web/src/app/dashboard/page.tsx +++ b/web/src/app/dashboard/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useMemo } from "react"; import Link from "next/link"; import { Download, @@ -147,66 +147,86 @@ export default function DashboardPage() { [dateRange] ); - // Apply region filter on top of date filter - const dateFiltered = applyDateFilter(results); - const filteredResults = - selectedRegion === "all" + // Apply date + region filters — memoized so they only recompute when deps change + const filteredResults = useMemo(() => { + const dateFiltered = applyDateFilter(results); + return selectedRegion === "all" ? dateFiltered : dateFiltered.filter((r) => r.game_servers?.region === selectedRegion); + }, [results, applyDateFilter, selectedRegion]); + + // Aggregate stats — memoized on filteredResults + const avgPing = useMemo( + () => + filteredResults.length > 0 + ? Math.round( + filteredResults.reduce((sum, r) => sum + r.ping_avg, 0) / + filteredResults.length + ) + : 0, + [filteredResults] + ); - const avgPing = - filteredResults.length > 0 - ? Math.round( - filteredResults.reduce((sum, r) => sum + r.ping_avg, 0) / + const avgPacketLoss = useMemo( + () => + filteredResults.length > 0 + ? ( + filteredResults.reduce((sum, r) => sum + r.packet_loss, 0) / filteredResults.length - ) - : 0; - - const avgPacketLoss = - filteredResults.length > 0 - ? ( - filteredResults.reduce((sum, r) => sum + r.packet_loss, 0) / - filteredResults.length - ).toFixed(1) - : "0"; - - const avgJitter = - filteredResults.length > 0 - ? ( - filteredResults.reduce((sum, r) => sum + (r.jitter || 0), 0) / - filteredResults.length - ).toFixed(1) - : "0"; - - // Get unique regions (from all results, not filtered) - const regions = [ - ...new Set(results.map((r) => r.game_servers?.region).filter(Boolean)), - ]; + ).toFixed(1) + : "0", + [filteredResults] + ); - // Prepare chart data - const chartData = filteredResults.slice(0, 20).reverse().map((r, i) => ({ - name: `Test ${i + 1}`, - ping: r.ping_avg, - jitter: r.jitter || 0, - loss: r.packet_loss, - })); - - // Server comparison data - const serverData = filteredResults.reduce((acc, r) => { - const location = r.game_servers?.location || "Unknown"; - if (!acc[location]) { - acc[location] = { pings: [], location }; - } - acc[location].pings.push(r.ping_avg); - return acc; - }, {} as Record); - - const serverChartData = Object.values(serverData) - .map((s) => ({ - name: s.location.split(" ")[0], - ping: Math.round(s.pings.reduce((a, b) => a + b, 0) / s.pings.length), - })) - .sort((a, b) => a.ping - b.ping); + const avgJitter = useMemo( + () => + filteredResults.length > 0 + ? ( + filteredResults.reduce((sum, r) => sum + (r.jitter || 0), 0) / + filteredResults.length + ).toFixed(1) + : "0", + [filteredResults] + ); + + // Unique regions derived from all results (unaffected by filters) + const regions = useMemo( + () => [ + ...new Set(results.map((r) => r.game_servers?.region).filter(Boolean)), + ], + [results] + ); + + // Ping history chart data + const chartData = useMemo( + () => + filteredResults.slice(0, 20).reverse().map((r, i) => ({ + name: `Test ${i + 1}`, + ping: r.ping_avg, + jitter: r.jitter || 0, + loss: r.packet_loss, + })), + [filteredResults] + ); + + // Server comparison chart data — group by location, sort by avg ping + const serverChartData = useMemo(() => { + const serverData = filteredResults.reduce((acc, r) => { + const location = r.game_servers?.location || "Unknown"; + if (!acc[location]) { + acc[location] = { pings: [], location }; + } + acc[location].pings.push(r.ping_avg); + return acc; + }, {} as Record); + + return Object.values(serverData) + .map((s) => ({ + name: s.location.split(" ")[0], + ping: Math.round(s.pings.reduce((a, b) => a + b, 0) / s.pings.length), + })) + .sort((a, b) => a.ping - b.ping); + }, [filteredResults]); const getQualityColor = (ping: number) => { if (ping < 30) return "text-green-500";