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. * Columns: Time, IP, Service, Country, Jail, Ban Count.
*/ */
import { useMemo } from "react"; import { memo, useMemo } from "react";
import { import {
Badge, Badge,
Button, 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.timeRange - Active time-range preset from the parent page.
* @param props.origin - Active origin filter 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 styles = useStyles();
const { banItems, total, page, setPage, loading, error, refresh } = useBans(timeRange, origin, source); 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>
</div> </div>
); );
} });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import { import {
memo,
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
@@ -130,7 +131,43 @@ interface WorldMapProps {
thresholdHigh?: number; 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, countries,
countryNames, countryNames,
selectedCountry, selectedCountry,
@@ -430,3 +467,5 @@ export function WorldMap({
</div> </div>
); );
} }
export const WorldMap = memo(WorldMapComponent, areWorldMapPropsEqual);