From 8134b168e162fa3f0ab2a44a40136531aa5a85f4 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Mon, 15 Jun 2026 16:12:08 +0200 Subject: [PATCH] perf(beszel): deep memoization pass for live chart rendering - Set withDots={false} to eliminate thousands of hidden SVG Dot nodes - Disable Recharts animation (isAnimationActive: false) during data updates - Merge ChartPanel + BeszelAreaChart into BeszelChartPanel to fix React.memo being defeated by children prop creating new refs - Memoize yAxisProps inside BeszelAreaChart instead of rebuilding inline - Hoist chartStyle, panelStyle, CPU_Y_AXIS_DOMAIN to module constants - Memoize subscription input/enabled in useLiveStats - Replace O(n) find() with Map lookup in useDockerChartData hot path --- .../src/beszel-system-stats/component.tsx | 120 ++++++------------ packages/widgets/src/beszel/_shared/chart.tsx | 61 +++++---- .../src/beszel/_shared/use-live-stats.ts | 14 +- 3 files changed, 88 insertions(+), 107 deletions(-) diff --git a/packages/widgets/src/beszel-system-stats/component.tsx b/packages/widgets/src/beszel-system-stats/component.tsx index cb17fcc3f..d5517e6ef 100644 --- a/packages/widgets/src/beszel-system-stats/component.tsx +++ b/packages/widgets/src/beszel-system-stats/component.tsx @@ -12,8 +12,8 @@ import classes from "./component.module.css"; import type { WidgetComponentProps } from "../definition"; import { containerColors } from "../beszel/_shared/colors"; import { - BeszelAreaChart, - ChartPanel, + BeszelChartPanel, + CPU_Y_AXIS_DOMAIN, useContainerNames, useDockerChartData, useSystemChartData, @@ -229,105 +229,69 @@ export default function BeszelSystemStatsWidget({ {activeStats && ( {options.showCpu && cpuData.length > 0 && ( - - - + )} {options.showMemory && memoryData.length > 0 && ( - - - + )} {options.showDisk && diskData.length > 0 && ( - - - + )} {options.showDiskIO && diskIOData.length > 0 && ( - - - + )} {options.showNetwork && networkData.length > 0 && ( - - - + )} {showDocker && containerSeries.length > 0 && ( <> {options.showDockerCpu && dockerCpuData.length > 0 && ( - - - + )} {options.showDockerMemory && dockerMemData.length > 0 && ( - - - + )} {options.showDockerNetwork && dockerNetData.length > 0 && ( - - - + )} )} diff --git a/packages/widgets/src/beszel/_shared/chart.tsx b/packages/widgets/src/beszel/_shared/chart.tsx index ca539b532..83434459e 100644 --- a/packages/widgets/src/beszel/_shared/chart.tsx +++ b/packages/widgets/src/beszel/_shared/chart.tsx @@ -19,17 +19,19 @@ function prepareRecords(records: T[], live: boolean) { } const yAxisBase = { tickMargin: 0, tick: { fontSize: 10 } } as const; -const activeDot = { r: 1, strokeWidth: 3 } as const; -const hiddenDot = { r: 0, strokeWidth: 0 } as const; +const chartStyle = { minWidth: 0, minHeight: 0 } as const; +const panelStyle = { minWidth: 0, overflow: "hidden" } as const; +const noAnimation = { isAnimationActive: false } as const; +export const CPU_Y_AXIS_DOMAIN: [number, string] = [0, "auto"]; -interface ChartPanelProps { +interface BeszelChartPanelProps { title: string; subtitle?: string; - children: React.ReactNode; + chartProps: BeszelAreaChartProps; } -export const ChartPanel = memo(({ title, subtitle, children }: ChartPanelProps) => ( - +export const BeszelChartPanel = memo(({ title, subtitle, chartProps }: BeszelChartPanelProps) => ( + {title} @@ -40,7 +42,7 @@ export const ChartPanel = memo(({ title, subtitle, children }: ChartPanelProps) )} - {children} + )); @@ -49,35 +51,45 @@ type BeszelAreaChartProps = Omit ( +const BeszelAreaChart = memo(({ + yAxisFormatter, + yAxisDomain, + yAxisProps: yAxisPropsOverride, + type = "default", + ...props +}: BeszelAreaChartProps) => { + const mergedYAxis = useMemo( + () => ({ + ...yAxisBase, + width: 32, + tickMargin: 2, + ...(yAxisDomain ? { domain: yAxisDomain } : {}), + tickFormatter: yAxisFormatter, + ...yAxisPropsOverride, + }), + [yAxisFormatter, yAxisDomain, yAxisPropsOverride], + ); + + return ( - ), -); + ); +}); export const useSystemChartData = ( systemStats: BeszelSystemStatsRecord[] | undefined, @@ -135,8 +147,9 @@ export const useDockerChartData = ( const { fmt, ordered } = prepareRecords(containerStats, live); return ordered.map((record) => { const point: Record = { time: fmt(record.created) }; + const byName = new Map(record.stats.map((c) => [c.n, c])); for (const name of containerNames) { - point[name] = extract(record.stats.find((c) => c.n === name)); + point[name] = extract(byName.get(name)); } return point; }); diff --git a/packages/widgets/src/beszel/_shared/use-live-stats.ts b/packages/widgets/src/beszel/_shared/use-live-stats.ts index 7cd103254..b006ab0fd 100644 --- a/packages/widgets/src/beszel/_shared/use-live-stats.ts +++ b/packages/widgets/src/beszel/_shared/use-live-stats.ts @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { clientApi } from "@homarr/api/client"; import type { BeszelContainerStatsRecord, BeszelSystemStatsRecord } from "@homarr/integrations/types"; @@ -41,10 +41,14 @@ export const useLiveStats = (integrationIds: string[], systemId: string, enabled setError(err instanceof Error ? err : new Error(String(err))); }, []); - clientApi.widget.beszel.subscribeSystemStats.useSubscription( - { integrationIds, systemId }, - { enabled: enabled && systemId !== "", onData, onError }, - ); + const subscriptionInput = useMemo(() => ({ integrationIds, systemId }), [integrationKey, systemId]); + const subscriptionEnabled = enabled && systemId !== ""; + + clientApi.widget.beszel.subscribeSystemStats.useSubscription(subscriptionInput, { + enabled: subscriptionEnabled, + onData, + onError, + }); return { data, error }; };