perf(beszel): throttle live renders, reduce buffer, skip hidden charts

- Decouple 1Hz SSE ingestion from render rate: accumulate data in a
  ref and flush to React state every 3s instead of every tick
- First data point flushes immediately to avoid blank screen
- Reduce MAX_BUFFER from 60 to 30 (halves SVG reconciliation work)
- Gate chart data hooks behind visibility flags so disabled charts
  skip all array processing
This commit is contained in:
Thomas Camlong committed 2026-06-15 17:14:18 +02:00
1 parent 7ffb90abd2
commit 9abce89a14
2 files changed
+53 -19

No files matched your search

@@ -129,16 +129,19 @@ export default function BeszelSystemStatsWidget({
[t],
);
const cpuData = useSystemChartData(activeStats?.systemStats, cpuMap, isLive);
const memoryData = useSystemChartData(activeStats?.systemStats, memoryMap, isLive);
const diskData = useSystemChartData(activeStats?.systemStats, diskMap, isLive);
const diskIOData = useSystemChartData(activeStats?.systemStats, diskIOMap, isLive);
const networkData = useSystemChartData(activeStats?.systemStats, networkMap, isLive);
const systemStats = activeStats?.systemStats;
const containerStatsRaw = activeStats?.containerStats;
const containerNames = useContainerNames(activeStats?.containerStats);
const dockerCpuData = useDockerChartData(activeStats?.containerStats, containerNames, "cpu", isLive);
const dockerMemData = useDockerChartData(activeStats?.containerStats, containerNames, "memory", isLive);
const dockerNetData = useDockerChartData(activeStats?.containerStats, containerNames, "network", isLive);
const cpuData = useSystemChartData(options.showCpu && systemStats || undefined, cpuMap, isLive);
const memoryData = useSystemChartData(options.showMemory && systemStats || undefined, memoryMap, isLive);
const diskData = useSystemChartData(options.showDisk && systemStats || undefined, diskMap, isLive);
const diskIOData = useSystemChartData(options.showDiskIO && systemStats || undefined, diskIOMap, isLive);
const networkData = useSystemChartData(options.showNetwork && systemStats || undefined, networkMap, isLive);
const containerNames = useContainerNames(showDocker && containerStatsRaw || undefined);
const dockerCpuData = useDockerChartData(options.showDockerCpu && containerStatsRaw || undefined, containerNames, "cpu", isLive);
const dockerMemData = useDockerChartData(options.showDockerMemory && containerStatsRaw || undefined, containerNames, "memory", isLive);
const dockerNetData = useDockerChartData(options.showDockerNetwork && containerStatsRaw || undefined, containerNames, "network", isLive);
const containerSeries = useMemo(
() => containerNames.map((name, i) => ({ name, color: containerColors[i % containerColors.length] as string })),
@@ -1,48 +1,79 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { clientApi } from "@homarr/api/client";
import type { BeszelContainerStatsRecord, BeszelSystemStatsRecord } from "@homarr/integrations/types";
const MAX_BUFFER = 60;
const MAX_BUFFER = 30;
const FLUSH_INTERVAL_MS = 3000;
interface LiveStatsData {
systemStats: BeszelSystemStatsRecord[];
containerStats: BeszelContainerStatsRecord[];
}
function appendToBuffer(
prev: LiveStatsData | null,
incoming: { stats: BeszelSystemStatsRecord; containerStats: BeszelContainerStatsRecord | null },
): LiveStatsData {
const systemStats = [...(prev?.systemStats ?? []), incoming.stats].slice(-MAX_BUFFER);
const prevContainer = prev?.containerStats ?? [];
if (!incoming.containerStats) {
return { systemStats, containerStats: prevContainer };
}
return { systemStats, containerStats: [...prevContainer, incoming.containerStats].slice(-MAX_BUFFER) };
}
export const useLiveStats = (integrationIds: string[], systemId: string, enabled: boolean) => {
const [data, setData] = useState<LiveStatsData | null>(null);
const [error, setError] = useState<Error | null>(null);
const bufferRef = useRef<LiveStatsData | null>(null);
const dirtyRef = useRef(false);
const integrationKey = integrationIds.join(",");
useEffect(() => {
setData(null);
setError(null);
bufferRef.current = null;
dirtyRef.current = false;
}, [systemId, enabled, integrationKey]);
const onData = useCallback(
(incoming: { stats: BeszelSystemStatsRecord; containerStats: BeszelContainerStatsRecord | null }) => {
setError(null);
setData((prev) => {
const systemStats = [...(prev?.systemStats ?? []), incoming.stats].slice(-MAX_BUFFER);
const containerStats = incoming.containerStats
? [...(prev?.containerStats ?? []), incoming.containerStats].slice(-MAX_BUFFER)
: (prev?.containerStats ?? []);
return { systemStats, containerStats };
});
const isFirst = !bufferRef.current;
bufferRef.current = appendToBuffer(bufferRef.current, incoming);
dirtyRef.current = true;
if (isFirst) {
setData(bufferRef.current);
}
},
[],
);
const onError = useCallback((err: unknown) => {
setError(err instanceof Error ? err : new Error(String(err)));
if (err instanceof Error) {
setError(err);
return;
}
setError(new Error(String(err)));
}, []);
const subscriptionInput = useMemo(() => ({ integrationIds, systemId }), [integrationKey, systemId]);
const subscriptionEnabled = enabled && systemId !== "";
useEffect(() => {
if (!subscriptionEnabled) return;
const id = setInterval(() => {
if (!dirtyRef.current || !bufferRef.current) return;
dirtyRef.current = false;
setData(bufferRef.current);
}, FLUSH_INTERVAL_MS);
return () => clearInterval(id);
}, [subscriptionEnabled]);
clientApi.widget.beszel.subscribeSystemStats.useSubscription(subscriptionInput, {
enabled: subscriptionEnabled,
onData,