perf(beszel): fix live mode re-render cascade for Docker charts
- Stabilize useContainerNames with ref comparison so it returns the same array reference when container names haven't changed - Memoize containerSeries (was computed inline every render) - Hoist all system chart series arrays into useMemo - Hoist activeDotProps/dotProps to module-level constants - Wrap BeszelAreaChart and ChartPanel in React.memo
This commit is contained in:
2 files changed
+52
-29
No files matched your search
@@ -105,6 +105,30 @@ export default function BeszelSystemStatsWidget({
|
||||
[t],
|
||||
);
|
||||
|
||||
const cpuSeries = useMemo(() => [{ name: t("chart.cpu.series"), color: "teal.6" }], [t]);
|
||||
const memorySeries = useMemo(
|
||||
() => [
|
||||
{ name: t("chart.memory.series"), color: "teal.5" },
|
||||
{ name: t("chart.memory.cache"), color: "teal.8" },
|
||||
],
|
||||
[t],
|
||||
);
|
||||
const diskSeries = useMemo(() => [{ name: t("chart.disk.series"), color: "grape.6" }], [t]);
|
||||
const diskIOSeries = useMemo(
|
||||
() => [
|
||||
{ name: t("chart.diskIO.write"), color: "orange.6" },
|
||||
{ name: t("chart.diskIO.read"), color: "blue.6" },
|
||||
],
|
||||
[t],
|
||||
);
|
||||
const networkSeries = useMemo(
|
||||
() => [
|
||||
{ name: t("chart.network.sent"), color: "blue.6" },
|
||||
{ name: t("chart.network.recv"), color: "teal.6" },
|
||||
],
|
||||
[t],
|
||||
);
|
||||
|
||||
const cpuData = useSystemChartData(activeStats?.systemStats, cpuMap, isLive);
|
||||
const memoryData = useSystemChartData(activeStats?.systemStats, memoryMap, isLive);
|
||||
const diskData = useSystemChartData(activeStats?.systemStats, diskMap, isLive);
|
||||
@@ -116,10 +140,10 @@ export default function BeszelSystemStatsWidget({
|
||||
const dockerMemData = useDockerChartData(activeStats?.containerStats, containerNames, "memory", isLive);
|
||||
const dockerNetData = useDockerChartData(activeStats?.containerStats, containerNames, "network", isLive);
|
||||
|
||||
const containerSeries = containerNames.map((name, i) => ({
|
||||
name,
|
||||
color: containerColors[i % containerColors.length] as string,
|
||||
}));
|
||||
const containerSeries = useMemo(
|
||||
() => containerNames.map((name, i) => ({ name, color: containerColors[i % containerColors.length] as string })),
|
||||
[containerNames],
|
||||
);
|
||||
|
||||
const cols = width > 600 ? 2 : 1;
|
||||
|
||||
@@ -209,7 +233,7 @@ export default function BeszelSystemStatsWidget({
|
||||
<BeszelAreaChart
|
||||
h={CHART_HEIGHT}
|
||||
data={cpuData}
|
||||
series={[{ name: t("chart.cpu.series"), color: "teal.6" }]}
|
||||
series={cpuSeries}
|
||||
yAxisFormatter={chartAxisFormatters.percent}
|
||||
yAxisDomain={[0, "auto"]}
|
||||
tooltipProps={tooltipPercent}
|
||||
@@ -223,10 +247,7 @@ export default function BeszelSystemStatsWidget({
|
||||
h={CHART_HEIGHT}
|
||||
data={memoryData}
|
||||
type="stacked"
|
||||
series={[
|
||||
{ name: t("chart.memory.series"), color: "teal.5" },
|
||||
{ name: t("chart.memory.cache"), color: "teal.8" },
|
||||
]}
|
||||
series={memorySeries}
|
||||
yAxisFormatter={chartAxisFormatters.gb}
|
||||
tooltipProps={tooltipGB}
|
||||
/>
|
||||
@@ -238,7 +259,7 @@ export default function BeszelSystemStatsWidget({
|
||||
<BeszelAreaChart
|
||||
h={CHART_HEIGHT}
|
||||
data={diskData}
|
||||
series={[{ name: t("chart.disk.series"), color: "grape.6" }]}
|
||||
series={diskSeries}
|
||||
yAxisFormatter={chartAxisFormatters.gb}
|
||||
tooltipProps={tooltipGB}
|
||||
/>
|
||||
@@ -250,10 +271,7 @@ export default function BeszelSystemStatsWidget({
|
||||
<BeszelAreaChart
|
||||
h={CHART_HEIGHT}
|
||||
data={diskIOData}
|
||||
series={[
|
||||
{ name: t("chart.diskIO.write"), color: "orange.6" },
|
||||
{ name: t("chart.diskIO.read"), color: "blue.6" },
|
||||
]}
|
||||
series={diskIOSeries}
|
||||
yAxisFormatter={chartAxisFormatters.rate}
|
||||
tooltipProps={tooltipRate}
|
||||
/>
|
||||
@@ -265,10 +283,7 @@ export default function BeszelSystemStatsWidget({
|
||||
<BeszelAreaChart
|
||||
h={CHART_HEIGHT}
|
||||
data={networkData}
|
||||
series={[
|
||||
{ name: t("chart.network.sent"), color: "blue.6" },
|
||||
{ name: t("chart.network.recv"), color: "teal.6" },
|
||||
]}
|
||||
series={networkSeries}
|
||||
yAxisFormatter={chartAxisFormatters.rate}
|
||||
tooltipProps={tooltipRate}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { memo, useMemo, useRef } from "react";
|
||||
import { Group, Stack, Text } from "@mantine/core";
|
||||
import type { AreaChartProps } from "@mantine/charts";
|
||||
import { AreaChart } from "@mantine/charts";
|
||||
@@ -19,6 +19,8 @@ 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;
|
||||
|
||||
interface ChartPanelProps {
|
||||
title: string;
|
||||
@@ -26,7 +28,7 @@ interface ChartPanelProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ChartPanel = ({ title, subtitle, children }: ChartPanelProps) => (
|
||||
export const ChartPanel = memo(({ title, subtitle, children }: ChartPanelProps) => (
|
||||
<Stack gap={4} style={{ minWidth: 0, overflow: "hidden" }}>
|
||||
<Group gap="xs">
|
||||
<Text size="sm" fw={600}>
|
||||
@@ -40,14 +42,14 @@ export const ChartPanel = ({ title, subtitle, children }: ChartPanelProps) => (
|
||||
</Group>
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
));
|
||||
|
||||
type BeszelAreaChartProps = Omit<AreaChartProps, "dataKey" | "curveType" | "withDots" | "withXAxis" | "withYAxis"> & {
|
||||
yAxisFormatter: (value: number) => string;
|
||||
yAxisDomain?: [number, string];
|
||||
};
|
||||
|
||||
export const BeszelAreaChart = ({
|
||||
export const BeszelAreaChart = memo(({
|
||||
yAxisFormatter,
|
||||
yAxisDomain,
|
||||
yAxisProps,
|
||||
@@ -64,8 +66,8 @@ export const BeszelAreaChart = ({
|
||||
type={type}
|
||||
strokeWidth={1}
|
||||
fillOpacity={0.2}
|
||||
activeDotProps={{ r: 1, strokeWidth: 3 }}
|
||||
dotProps={{ r: 0, strokeWidth: 0 }}
|
||||
activeDotProps={activeDot}
|
||||
dotProps={hiddenDot}
|
||||
withXAxis
|
||||
withYAxis
|
||||
w="100%"
|
||||
@@ -80,7 +82,7 @@ export const BeszelAreaChart = ({
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
));
|
||||
|
||||
export const useSystemChartData = (
|
||||
systemStats: BeszelSystemStatsRecord[] | undefined,
|
||||
@@ -96,17 +98,23 @@ export const useSystemChartData = (
|
||||
}));
|
||||
}, [systemStats, mapFn, live]);
|
||||
|
||||
export const useContainerNames = (containerStats: BeszelContainerStatsRecord[] | undefined, max = 15) =>
|
||||
useMemo(() => {
|
||||
if (!containerStats?.length) return [];
|
||||
export const useContainerNames = (containerStats: BeszelContainerStatsRecord[] | undefined, max = 15) => {
|
||||
const prevRef = useRef<string[]>([]);
|
||||
return useMemo(() => {
|
||||
if (!containerStats?.length) return prevRef.current.length === 0 ? prevRef.current : (prevRef.current = []);
|
||||
const names = new Set<string>();
|
||||
for (const record of containerStats) {
|
||||
for (const c of record.stats) {
|
||||
names.add(c.n);
|
||||
}
|
||||
}
|
||||
return [...names].slice(0, max);
|
||||
const next = [...names].slice(0, max);
|
||||
const prev = prevRef.current;
|
||||
if (prev.length === next.length && prev.every((n, i) => n === next[i])) return prev;
|
||||
prevRef.current = next;
|
||||
return next;
|
||||
}, [containerStats, max]);
|
||||
};
|
||||
|
||||
type ContainerExtractor = (container: BeszelContainerStatsRecord["stats"][number] | undefined) => number;
|
||||
|
||||
|
||||
Reference in new issue
Block a user