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
This commit is contained in:
Thomas Camlong committed 2026-06-15 16:12:47 +02:00
1 parent 583b1d2935
commit 8134b168e1
3 files changed
+88 -107

No files matched your search

@@ -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 && (
<SimpleGrid cols={cols} spacing="md">
{options.showCpu && cpuData.length > 0 && (
<ChartPanel title={t("chart.cpu.title")} subtitle={t("chart.cpu.subtitle")}>
<BeszelAreaChart
h={CHART_HEIGHT}
data={cpuData}
series={cpuSeries}
yAxisFormatter={chartAxisFormatters.percent}
yAxisDomain={[0, "auto"]}
tooltipProps={tooltipPercent}
/>
</ChartPanel>
<BeszelChartPanel
title={t("chart.cpu.title")}
subtitle={t("chart.cpu.subtitle")}
chartProps={{ h: CHART_HEIGHT, data: cpuData, series: cpuSeries, yAxisFormatter: chartAxisFormatters.percent, yAxisDomain: CPU_Y_AXIS_DOMAIN, tooltipProps: tooltipPercent }}
/>
)}
{options.showMemory && memoryData.length > 0 && (
<ChartPanel title={t("chart.memory.title")} subtitle={t("chart.memory.subtitle")}>
<BeszelAreaChart
h={CHART_HEIGHT}
data={memoryData}
type="stacked"
series={memorySeries}
yAxisFormatter={chartAxisFormatters.gb}
tooltipProps={tooltipGB}
/>
</ChartPanel>
<BeszelChartPanel
title={t("chart.memory.title")}
subtitle={t("chart.memory.subtitle")}
chartProps={{ h: CHART_HEIGHT, data: memoryData, type: "stacked", series: memorySeries, yAxisFormatter: chartAxisFormatters.gb, tooltipProps: tooltipGB }}
/>
)}
{options.showDisk && diskData.length > 0 && (
<ChartPanel title={t("chart.disk.title")} subtitle={t("chart.disk.subtitle")}>
<BeszelAreaChart
h={CHART_HEIGHT}
data={diskData}
series={diskSeries}
yAxisFormatter={chartAxisFormatters.gb}
tooltipProps={tooltipGB}
/>
</ChartPanel>
<BeszelChartPanel
title={t("chart.disk.title")}
subtitle={t("chart.disk.subtitle")}
chartProps={{ h: CHART_HEIGHT, data: diskData, series: diskSeries, yAxisFormatter: chartAxisFormatters.gb, tooltipProps: tooltipGB }}
/>
)}
{options.showDiskIO && diskIOData.length > 0 && (
<ChartPanel title={t("chart.diskIO.title")} subtitle={t("chart.diskIO.subtitle")}>
<BeszelAreaChart
h={CHART_HEIGHT}
data={diskIOData}
series={diskIOSeries}
yAxisFormatter={chartAxisFormatters.rate}
tooltipProps={tooltipRate}
/>
</ChartPanel>
<BeszelChartPanel
title={t("chart.diskIO.title")}
subtitle={t("chart.diskIO.subtitle")}
chartProps={{ h: CHART_HEIGHT, data: diskIOData, series: diskIOSeries, yAxisFormatter: chartAxisFormatters.rate, tooltipProps: tooltipRate }}
/>
)}
{options.showNetwork && networkData.length > 0 && (
<ChartPanel title={t("chart.network.title")} subtitle={t("chart.network.subtitle")}>
<BeszelAreaChart
h={CHART_HEIGHT}
data={networkData}
series={networkSeries}
yAxisFormatter={chartAxisFormatters.rate}
tooltipProps={tooltipRate}
/>
</ChartPanel>
<BeszelChartPanel
title={t("chart.network.title")}
subtitle={t("chart.network.subtitle")}
chartProps={{ h: CHART_HEIGHT, data: networkData, series: networkSeries, yAxisFormatter: chartAxisFormatters.rate, tooltipProps: tooltipRate }}
/>
)}
{showDocker && containerSeries.length > 0 && (
<>
{options.showDockerCpu && dockerCpuData.length > 0 && (
<ChartPanel title={t("chart.dockerCpu.title")} subtitle={t("chart.dockerCpu.subtitle")}>
<BeszelAreaChart
h={CHART_HEIGHT}
data={dockerCpuData}
type="stacked"
series={containerSeries}
yAxisFormatter={chartAxisFormatters.percent}
tooltipProps={tooltipPercentTotal}
/>
</ChartPanel>
<BeszelChartPanel
title={t("chart.dockerCpu.title")}
subtitle={t("chart.dockerCpu.subtitle")}
chartProps={{ h: CHART_HEIGHT, data: dockerCpuData, type: "stacked", series: containerSeries, yAxisFormatter: chartAxisFormatters.percent, tooltipProps: tooltipPercentTotal }}
/>
)}
{options.showDockerMemory && dockerMemData.length > 0 && (
<ChartPanel title={t("chart.dockerMemory.title")} subtitle={t("chart.dockerMemory.subtitle")}>
<BeszelAreaChart
h={CHART_HEIGHT}
data={dockerMemData}
type="stacked"
series={containerSeries}
yAxisFormatter={chartAxisFormatters.bytes}
tooltipProps={tooltipBytesTotal}
/>
</ChartPanel>
<BeszelChartPanel
title={t("chart.dockerMemory.title")}
subtitle={t("chart.dockerMemory.subtitle")}
chartProps={{ h: CHART_HEIGHT, data: dockerMemData, type: "stacked", series: containerSeries, yAxisFormatter: chartAxisFormatters.bytes, tooltipProps: tooltipBytesTotal }}
/>
)}
{options.showDockerNetwork && dockerNetData.length > 0 && (
<ChartPanel title={t("chart.dockerNetwork.title")} subtitle={t("chart.dockerNetwork.subtitle")}>
<BeszelAreaChart
h={CHART_HEIGHT}
data={dockerNetData}
series={containerSeries}
yAxisFormatter={chartAxisFormatters.rate}
tooltipProps={tooltipRate}
/>
</ChartPanel>
<BeszelChartPanel
title={t("chart.dockerNetwork.title")}
subtitle={t("chart.dockerNetwork.subtitle")}
chartProps={{ h: CHART_HEIGHT, data: dockerNetData, series: containerSeries, yAxisFormatter: chartAxisFormatters.rate, tooltipProps: tooltipRate }}
/>
)}
</>
)}
+37 -24
View File
@@ -19,17 +19,19 @@ function prepareRecords<T>(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) => (
<Stack gap={4} style={{ minWidth: 0, overflow: "hidden" }}>
export const BeszelChartPanel = memo(({ title, subtitle, chartProps }: BeszelChartPanelProps) => (
<Stack gap={4} style={panelStyle}>
<Group gap="xs">
<Text size="sm" fw={600}>
{title}
@@ -40,7 +42,7 @@ export const ChartPanel = memo(({ title, subtitle, children }: ChartPanelProps)
</Text>
)}
</Group>
{children}
<BeszelAreaChart {...chartProps} />
</Stack>
));
@@ -49,35 +51,45 @@ type BeszelAreaChartProps = Omit<AreaChartProps, "dataKey" | "curveType" | "with
yAxisDomain?: [number, string];
};
export const BeszelAreaChart = memo(
({ yAxisFormatter, yAxisDomain, yAxisProps, style, type = "default", ...props }: BeszelAreaChartProps) => (
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 (
<AreaChart
dataKey="time"
curveType="monotone"
withGradient={false}
connectNulls
withDots={true}
withDots={false}
type={type}
strokeWidth={1}
fillOpacity={0.2}
activeDotProps={activeDot}
dotProps={hiddenDot}
areaChartProps={noAnimation}
withXAxis
withYAxis
w="100%"
style={{ minWidth: 0, minHeight: 0, ...style }}
yAxisProps={{
...yAxisBase,
width: 32,
tickMargin: 2,
...(yAxisDomain ? { domain: yAxisDomain } : {}),
tickFormatter: yAxisFormatter,
...yAxisProps,
}}
style={chartStyle}
yAxisProps={mergedYAxis}
{...props}
/>
),
);
);
});
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<string, unknown> = { 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;
});
@@ -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 };
};