Improve metrics UI performance (#22691)

* embed cpu/mem stats into detectors, cameras, and processes

so history consumers don't need the full cpu_usages dict

* support dot-notation for nested keys

to avoid returning large objects when only specific subfields are needed

* fix setLastUpdated being called inside useMemo

this triggered a setState-during-render warning, so moved to a useEffect

* frontend types

* frontend

hide instead of unmount all graphs - re-rendering is much more expensive and disruptive than the amount of dom memory required

keep track of visited tabs to keep them mounted rather than re-mounting or mounting all tabs

add isActive prop to all charts to re-trigger animation when switching metrics tabs

fix chart data padding bug where the loop used number of series rather than number of data points

fix bug where only a shallow copy of the array was used for mutation

fix missing key prop causing console logs

* add isactive after rebase

* formatting

* skip None values in filtered output for dot notation
This commit is contained in:
Josh Hawkins
2026-03-29 11:58:47 -06:00
committed by GitHub
parent f002513d36
commit f44f485f48
10 changed files with 330 additions and 93 deletions
+25 -1
View File
@@ -2,7 +2,7 @@ import { useTheme } from "@/context/theme-provider";
import { useDateLocale } from "@/hooks/use-date-locale";
import { FrigateConfig } from "@/types/frigateConfig";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import { useCallback, useEffect, useMemo } from "react";
import { useCallback, useEffect, useMemo, useRef } from "react";
import Chart from "react-apexcharts";
import { isMobileOnly } from "react-device-detect";
import { useTranslation } from "react-i18next";
@@ -17,6 +17,7 @@ type CameraLineGraphProps = {
dataLabels: string[];
updateTimes: number[];
data: ApexAxisChartSeries;
isActive?: boolean;
};
export function CameraLineGraph({
graphId,
@@ -24,6 +25,7 @@ export function CameraLineGraph({
dataLabels,
updateTimes,
data,
isActive = true,
}: CameraLineGraphProps) {
const { t } = useTranslation(["views/system", "common"]);
const { data: config } = useSWR<FrigateConfig>("config", {
@@ -134,6 +136,16 @@ export function CameraLineGraph({
ApexCharts.exec(graphId, "updateOptions", options, true, true);
}, [graphId, options]);
const hasBeenActive = useRef(isActive);
useEffect(() => {
if (isActive && hasBeenActive.current === false) {
ApexCharts.exec(graphId, "updateSeries", data, true);
}
hasBeenActive.current = isActive;
// only replay animation on visibility change, not data updates
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isActive, graphId]);
return (
<div className="flex w-full flex-col">
{lastValues && (
@@ -166,6 +178,7 @@ type EventsPerSecondLineGraphProps = {
name: string;
updateTimes: number[];
data: ApexAxisChartSeries;
isActive?: boolean;
};
export function EventsPerSecondsLineGraph({
graphId,
@@ -173,6 +186,7 @@ export function EventsPerSecondsLineGraph({
name,
updateTimes,
data,
isActive = true,
}: EventsPerSecondLineGraphProps) {
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
@@ -277,6 +291,16 @@ export function EventsPerSecondsLineGraph({
ApexCharts.exec(graphId, "updateOptions", options, true, true);
}, [graphId, options]);
const hasBeenActive = useRef(isActive);
useEffect(() => {
if (isActive && hasBeenActive.current === false) {
ApexCharts.exec(graphId, "updateSeries", data, true);
}
hasBeenActive.current = isActive;
// only replay animation on visibility change, not data updates
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isActive, graphId]);
return (
<div className="flex w-full flex-col">
<div className="flex items-center gap-1">
+20 -6
View File
@@ -3,7 +3,7 @@ import { useDateLocale } from "@/hooks/use-date-locale";
import { FrigateConfig } from "@/types/frigateConfig";
import { Threshold } from "@/types/graph";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import { useCallback, useEffect, useMemo } from "react";
import { useCallback, useEffect, useMemo, useRef } from "react";
import Chart from "react-apexcharts";
import { isMobileOnly } from "react-device-detect";
import { useTranslation } from "react-i18next";
@@ -16,6 +16,7 @@ type ThresholdBarGraphProps = {
threshold: Threshold;
updateTimes: number[];
data: ApexAxisChartSeries;
isActive?: boolean;
};
export function ThresholdBarGraph({
graphId,
@@ -24,6 +25,7 @@ export function ThresholdBarGraph({
threshold,
updateTimes,
data,
isActive = true,
}: ThresholdBarGraphProps) {
const displayName = name || data[0]?.name || "";
const { data: config } = useSWR<FrigateConfig>("config", {
@@ -173,17 +175,29 @@ export function ThresholdBarGraph({
return data;
}
const copiedData = [...data];
const dataPointCount = data[0].data.length;
const fakeData = [];
for (let i = data.length; i < 30; i++) {
for (let i = dataPointCount; i < 30; i++) {
fakeData.push({ x: i - 30, y: 0 });
}
// @ts-expect-error data types are not obvious
copiedData[0].data = [...fakeData, ...data[0].data];
return copiedData;
const paddedFirst = {
...data[0],
data: [...fakeData, ...data[0].data],
};
return [paddedFirst, ...data.slice(1)] as ApexAxisChartSeries;
}, [data]);
const hasBeenActive = useRef(isActive);
useEffect(() => {
if (isActive && hasBeenActive.current === false) {
ApexCharts.exec(graphId, "updateSeries", chartData, true);
}
hasBeenActive.current = isActive;
// only replay animation on visibility change, not data updates
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isActive, graphId]);
return (
<div className="flex w-full flex-col">
<div className="flex items-center gap-1">