Skip to content

Commit 985949b

Browse files
committed
perf: memoize dashboard derived state to eliminate redundant renders
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.
1 parent b0f5b66 commit 985949b

1 file changed

Lines changed: 76 additions & 56 deletions

File tree

web/src/app/dashboard/page.tsx

Lines changed: 76 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useState, useEffect, useCallback } from "react";
3+
import { useState, useEffect, useCallback, useMemo } from "react";
44
import Link from "next/link";
55
import {
66
Download,
@@ -147,66 +147,86 @@ export default function DashboardPage() {
147147
[dateRange]
148148
);
149149

150-
// Apply region filter on top of date filter
151-
const dateFiltered = applyDateFilter(results);
152-
const filteredResults =
153-
selectedRegion === "all"
150+
// Apply date + region filters — memoized so they only recompute when deps change
151+
const filteredResults = useMemo(() => {
152+
const dateFiltered = applyDateFilter(results);
153+
return selectedRegion === "all"
154154
? dateFiltered
155155
: dateFiltered.filter((r) => r.game_servers?.region === selectedRegion);
156+
}, [results, applyDateFilter, selectedRegion]);
157+
158+
// Aggregate stats — memoized on filteredResults
159+
const avgPing = useMemo(
160+
() =>
161+
filteredResults.length > 0
162+
? Math.round(
163+
filteredResults.reduce((sum, r) => sum + r.ping_avg, 0) /
164+
filteredResults.length
165+
)
166+
: 0,
167+
[filteredResults]
168+
);
156169

157-
const avgPing =
158-
filteredResults.length > 0
159-
? Math.round(
160-
filteredResults.reduce((sum, r) => sum + r.ping_avg, 0) /
170+
const avgPacketLoss = useMemo(
171+
() =>
172+
filteredResults.length > 0
173+
? (
174+
filteredResults.reduce((sum, r) => sum + r.packet_loss, 0) /
161175
filteredResults.length
162-
)
163-
: 0;
164-
165-
const avgPacketLoss =
166-
filteredResults.length > 0
167-
? (
168-
filteredResults.reduce((sum, r) => sum + r.packet_loss, 0) /
169-
filteredResults.length
170-
).toFixed(1)
171-
: "0";
172-
173-
const avgJitter =
174-
filteredResults.length > 0
175-
? (
176-
filteredResults.reduce((sum, r) => sum + (r.jitter || 0), 0) /
177-
filteredResults.length
178-
).toFixed(1)
179-
: "0";
180-
181-
// Get unique regions (from all results, not filtered)
182-
const regions = [
183-
...new Set(results.map((r) => r.game_servers?.region).filter(Boolean)),
184-
];
176+
).toFixed(1)
177+
: "0",
178+
[filteredResults]
179+
);
185180

186-
// Prepare chart data
187-
const chartData = filteredResults.slice(0, 20).reverse().map((r, i) => ({
188-
name: `Test ${i + 1}`,
189-
ping: r.ping_avg,
190-
jitter: r.jitter || 0,
191-
loss: r.packet_loss,
192-
}));
193-
194-
// Server comparison data
195-
const serverData = filteredResults.reduce((acc, r) => {
196-
const location = r.game_servers?.location || "Unknown";
197-
if (!acc[location]) {
198-
acc[location] = { pings: [], location };
199-
}
200-
acc[location].pings.push(r.ping_avg);
201-
return acc;
202-
}, {} as Record<string, { pings: number[]; location: string }>);
203-
204-
const serverChartData = Object.values(serverData)
205-
.map((s) => ({
206-
name: s.location.split(" ")[0],
207-
ping: Math.round(s.pings.reduce((a, b) => a + b, 0) / s.pings.length),
208-
}))
209-
.sort((a, b) => a.ping - b.ping);
181+
const avgJitter = useMemo(
182+
() =>
183+
filteredResults.length > 0
184+
? (
185+
filteredResults.reduce((sum, r) => sum + (r.jitter || 0), 0) /
186+
filteredResults.length
187+
).toFixed(1)
188+
: "0",
189+
[filteredResults]
190+
);
191+
192+
// Unique regions derived from all results (unaffected by filters)
193+
const regions = useMemo(
194+
() => [
195+
...new Set(results.map((r) => r.game_servers?.region).filter(Boolean)),
196+
],
197+
[results]
198+
);
199+
200+
// Ping history chart data
201+
const chartData = useMemo(
202+
() =>
203+
filteredResults.slice(0, 20).reverse().map((r, i) => ({
204+
name: `Test ${i + 1}`,
205+
ping: r.ping_avg,
206+
jitter: r.jitter || 0,
207+
loss: r.packet_loss,
208+
})),
209+
[filteredResults]
210+
);
211+
212+
// Server comparison chart data — group by location, sort by avg ping
213+
const serverChartData = useMemo(() => {
214+
const serverData = filteredResults.reduce((acc, r) => {
215+
const location = r.game_servers?.location || "Unknown";
216+
if (!acc[location]) {
217+
acc[location] = { pings: [], location };
218+
}
219+
acc[location].pings.push(r.ping_avg);
220+
return acc;
221+
}, {} as Record<string, { pings: number[]; location: string }>);
222+
223+
return Object.values(serverData)
224+
.map((s) => ({
225+
name: s.location.split(" ")[0],
226+
ping: Math.round(s.pings.reduce((a, b) => a + b, 0) / s.pings.length),
227+
}))
228+
.sort((a, b) => a.ping - b.ping);
229+
}, [filteredResults]);
210230

211231
const getQualityColor = (ping: number) => {
212232
if (ping < 30) return "text-green-500";

0 commit comments

Comments
 (0)