Add React.memo to heavy dashboard components

This commit is contained in:
2026-04-20 20:00:59 +02:00
parent 1d6564aa32
commit 1694ac17f8
7 changed files with 58 additions and 18 deletions

View File

@@ -8,7 +8,7 @@
* Columns: Time, IP, Service, Country, Jail, Ban Count.
*/
import { useMemo } from "react";
import { memo, useMemo } from "react";
import {
Badge,
Button,
@@ -191,7 +191,7 @@ function buildBanColumns(styles: ReturnType<typeof useStyles>): TableColumnDefin
* @param props.timeRange - Active time-range preset from the parent page.
* @param props.origin - Active origin filter from the parent page.
*/
export function BanTable({ timeRange, origin = "all", source = "fail2ban" }: BanTableProps): React.JSX.Element {
export const BanTable = memo(function BanTable({ timeRange, origin = "all", source = "fail2ban" }: BanTableProps): React.JSX.Element {
const styles = useStyles();
const { banItems, total, page, setPage, loading, error, refresh } = useBans(timeRange, origin, source);
@@ -276,4 +276,4 @@ export function BanTable({ timeRange, origin = "all", source = "fail2ban" }: Ban
</div>
</div>
);
}
});

View File

@@ -4,7 +4,7 @@
* Calls `useBanTrend` internally and handles loading, error, and empty states.
*/
import { useMemo } from "react";
import { memo, useMemo } from "react";
import {
Area,
AreaChart,
@@ -194,7 +194,7 @@ function TrendTooltip(props: TrendTooltipProps): React.JSX.Element | null {
*
* @param props - `timeRange` and `origin` filter props.
*/
export function BanTrendChart({
export const BanTrendChart = memo(function BanTrendChart({
timeRange,
origin,
source = "fail2ban",
@@ -271,5 +271,5 @@ export function BanTrendChart({
</div>
</ChartStateWrapper>
);
}
});

View File

@@ -6,7 +6,7 @@
* empty states so the parent only needs to pass filter props.
*/
import { useMemo } from "react";
import { memo, useMemo } from "react";
import {
Bar,
BarChart,
@@ -129,7 +129,7 @@ function JailTooltip(props: TooltipContentProps): React.JSX.Element | null {
*
* @param props - `timeRange` and `origin` filter props.
*/
export function JailDistributionChart({
export const JailDistributionChart = memo(function JailDistributionChart({
timeRange,
origin,
}: JailDistributionChartProps): React.JSX.Element {
@@ -185,4 +185,4 @@ export function JailDistributionChart({
</div>
</ChartStateWrapper>
);
}
});

View File

@@ -9,6 +9,7 @@
* via the {@link useServerStatus} hook.
*/
import { memo } from "react";
import {
Badge,
Button,
@@ -68,7 +69,7 @@ const useStyles = makeStyles({
* Render this at the top of the dashboard page (and any page that should
* show live server status).
*/
export function ServerStatusBar(): React.JSX.Element {
export const ServerStatusBar = memo(function ServerStatusBar(): React.JSX.Element {
const styles = useStyles();
const { status, loading, error, refresh } = useServerStatus();
@@ -165,4 +166,4 @@ export function ServerStatusBar(): React.JSX.Element {
</Tooltip>
</div>
);
}
});

View File

@@ -3,7 +3,7 @@
* by ban count, sorted descending.
*/
import { useMemo } from "react";
import { memo, useMemo } from "react";
import {
Bar,
BarChart,
@@ -137,7 +137,7 @@ function BarTooltip(props: TooltipContentProps): React.JSX.Element | null {
* @param props - `countries` map and `countryNames` map from the
* `/api/dashboard/bans/by-country` response.
*/
export function TopCountriesBarChart({
export const TopCountriesBarChart = memo(function TopCountriesBarChart({
countries,
countryNames,
}: TopCountriesBarChartProps): React.JSX.Element {
@@ -193,4 +193,4 @@ export function TopCountriesBarChart({
</ResponsiveContainer>
</div>
);
}
});

View File

@@ -3,7 +3,7 @@
* an "Other" slice aggregating all remaining countries.
*/
import { useMemo } from "react";
import { memo, useMemo } from "react";
import {
Cell,
Legend,
@@ -129,7 +129,7 @@ function PieTooltip(props: TooltipContentProps): React.JSX.Element | null {
* @param props - `countries` map and `countryNames` map from the
* `/api/dashboard/bans/by-country` response.
*/
export function TopCountriesPieChart({
export const TopCountriesPieChart = memo(function TopCountriesPieChart({
countries,
countryNames,
}: TopCountriesPieChartProps): React.JSX.Element {
@@ -194,4 +194,4 @@ export function TopCountriesPieChart({
</ResponsiveContainer>
</div>
);
}
});

View File

@@ -7,6 +7,7 @@
import { createPortal } from "react-dom";
import {
memo,
useCallback,
useEffect,
useMemo,
@@ -130,7 +131,43 @@ interface WorldMapProps {
thresholdHigh?: number;
}
export function WorldMap({
function areStringRecordShallowEqual(
left: Record<string, string>,
right: Record<string, string>,
): boolean {
const leftKeys = Object.keys(left);
const rightKeys = Object.keys(right);
if (leftKeys.length !== rightKeys.length) {
return false;
}
return leftKeys.every((key) => left[key] === right[key]);
}
function areNumberRecordShallowEqual(
left: Record<string, number>,
right: Record<string, number>,
): boolean {
const leftKeys = Object.keys(left);
const rightKeys = Object.keys(right);
if (leftKeys.length !== rightKeys.length) {
return false;
}
return leftKeys.every((key) => left[key] === right[key]);
}
function areWorldMapPropsEqual(prev: WorldMapProps, next: WorldMapProps): boolean {
return (
prev.selectedCountry === next.selectedCountry &&
prev.onSelectCountry === next.onSelectCountry &&
prev.thresholdLow === next.thresholdLow &&
prev.thresholdMedium === next.thresholdMedium &&
prev.thresholdHigh === next.thresholdHigh &&
areNumberRecordShallowEqual(prev.countries, next.countries) &&
areStringRecordShallowEqual(prev.countryNames ?? {}, next.countryNames ?? {})
);
}
function WorldMapComponent({
countries,
countryNames,
selectedCountry,
@@ -430,3 +467,5 @@ export function WorldMap({
</div>
);
}
export const WorldMap = memo(WorldMapComponent, areWorldMapPropsEqual);