mirror of
https://github.com/vxcontrol/pentagi.git
synced 2026-08-22 19:16:29 +00:00
perf(frontend): drop Dashboard period-switch INP from 434ms to 134ms
The Analytics tab on /dashboard renders 4 Recharts plus a Flow
Execution Details list with 130+ Collapsible rows. Switching the period
(Week/Month/Quarter) re-rendered everything synchronously: the
pointerdown→paint interaction had INP=434ms (Google "Needs Improvement"
zone), with processing duration alone accounting for 403ms.
Four small changes, no new dependencies:
1. useMemo on usageChartData / toolcallsChartData / flowsChartData so
Recharts receives stable references when only an unrelated piece
of parent state changes (e.g. tooltip hover state).
2. useDeferredValue(executionStats) so a period switch can repaint
the charts first and reconcile the long list as a low-priority
follow-up. While the deferred value is stale we dim the list to
60% opacity, matching the existing dashboard.tsx convention.
3. content-visibility: auto + contain-intrinsic-size on every
FlowExecutionItem. Off-screen rows skip layout and paint entirely
— this is the cheapest possible virtualization, with no deps,
measurement, or scroll math.
4. React.memo on FlowExecutionItem and TaskExecutionItem so parent
re-renders don't traverse the entire 130-row tree.
Measured via chrome-devtools MCP performance trace:
Before: INP 434ms — input 1ms / processing 403ms / present 29ms
After: INP 134ms — input 1ms / processing 112ms / present 21ms
CLS stays at 0.00.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
1a8b04aa0d
commit
b7ed0689a9
@@ -1,6 +1,6 @@
|
||||
import { format } from 'date-fns';
|
||||
import { ChevronRight, Clock, Loader2, Wrench } from 'lucide-react';
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { memo, useDeferredValue, useMemo, useRef, useState } from 'react';
|
||||
import { Area, AreaChart, Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
import type { FlowFragmentFragment, UsageStatsPeriod } from '@/graphql/types';
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
useToolcallsStatsByPeriodQuery,
|
||||
useUsageStatsByPeriodQuery,
|
||||
} from '@/graphql/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { formatCost, formatDuration, formatNumber, formatTokenCount } from '@/lib/utils/format';
|
||||
|
||||
const CHART_COLORS = {
|
||||
@@ -86,31 +87,48 @@ export function DashboardAnalytics({ period }: { period: UsageStatsPeriod }) {
|
||||
return map;
|
||||
}, [flowsData?.flows]);
|
||||
|
||||
const usageChartData = [...(usageByPeriodData?.usageStatsByPeriod ?? [])].reverse().map((item) => ({
|
||||
cacheIn: item.stats.totalUsageCacheIn,
|
||||
costIn: item.stats.totalUsageCostIn,
|
||||
costOut: item.stats.totalUsageCostOut,
|
||||
date: item.date,
|
||||
tokensIn: item.stats.totalUsageIn,
|
||||
tokensOut: item.stats.totalUsageOut,
|
||||
totalCost: item.stats.totalUsageCostIn + item.stats.totalUsageCostOut,
|
||||
}));
|
||||
const usageChartData = useMemo(
|
||||
() =>
|
||||
[...(usageByPeriodData?.usageStatsByPeriod ?? [])].reverse().map((item) => ({
|
||||
cacheIn: item.stats.totalUsageCacheIn,
|
||||
costIn: item.stats.totalUsageCostIn,
|
||||
costOut: item.stats.totalUsageCostOut,
|
||||
date: item.date,
|
||||
tokensIn: item.stats.totalUsageIn,
|
||||
tokensOut: item.stats.totalUsageOut,
|
||||
totalCost: item.stats.totalUsageCostIn + item.stats.totalUsageCostOut,
|
||||
})),
|
||||
[usageByPeriodData?.usageStatsByPeriod],
|
||||
);
|
||||
|
||||
const toolcallsChartData = [...(toolcallsByPeriodData?.toolcallsStatsByPeriod ?? [])].reverse().map((item) => ({
|
||||
count: item.stats.totalCount,
|
||||
date: item.date,
|
||||
duration: item.stats.totalDurationSeconds,
|
||||
}));
|
||||
const toolcallsChartData = useMemo(
|
||||
() =>
|
||||
[...(toolcallsByPeriodData?.toolcallsStatsByPeriod ?? [])].reverse().map((item) => ({
|
||||
count: item.stats.totalCount,
|
||||
date: item.date,
|
||||
duration: item.stats.totalDurationSeconds,
|
||||
})),
|
||||
[toolcallsByPeriodData?.toolcallsStatsByPeriod],
|
||||
);
|
||||
|
||||
const flowsChartData = [...(flowsByPeriodData?.flowsStatsByPeriod ?? [])].reverse().map((item) => ({
|
||||
assistants: item.stats.totalAssistantsCount,
|
||||
date: item.date,
|
||||
flows: item.stats.totalFlowsCount,
|
||||
subtasks: item.stats.totalSubtasksCount,
|
||||
tasks: item.stats.totalTasksCount,
|
||||
}));
|
||||
const flowsChartData = useMemo(
|
||||
() =>
|
||||
[...(flowsByPeriodData?.flowsStatsByPeriod ?? [])].reverse().map((item) => ({
|
||||
assistants: item.stats.totalAssistantsCount,
|
||||
date: item.date,
|
||||
flows: item.stats.totalFlowsCount,
|
||||
subtasks: item.stats.totalSubtasksCount,
|
||||
tasks: item.stats.totalTasksCount,
|
||||
})),
|
||||
[flowsByPeriodData?.flowsStatsByPeriod],
|
||||
);
|
||||
|
||||
const executionStats = executionStatsData?.flowsExecutionStatsByPeriod ?? [];
|
||||
// Defer the heavy 130+ row list so a period switch can repaint the charts
|
||||
// first and run the long list reconciliation as a low-priority follow-up.
|
||||
// Combined with content-visibility: auto on each row this drops the
|
||||
// pointerdown→paint INP from ~430ms into the "Good" (<200ms) zone.
|
||||
const deferredExecutionStats = useDeferredValue(executionStats);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
@@ -342,13 +360,18 @@ export function DashboardAnalytics({ period }: { period: UsageStatsPeriod }) {
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="text-muted-foreground size-6 animate-spin" />
|
||||
</div>
|
||||
) : !executionStats.length ? (
|
||||
) : !deferredExecutionStats.length ? (
|
||||
<p className="text-muted-foreground py-8 text-center text-sm">
|
||||
No flow executions in this period
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{executionStats.map((flow) => (
|
||||
<div
|
||||
className={cn(
|
||||
'space-y-1 transition-opacity',
|
||||
deferredExecutionStats !== executionStats && 'opacity-60',
|
||||
)}
|
||||
>
|
||||
{deferredExecutionStats.map((flow) => (
|
||||
<FlowExecutionItem
|
||||
flow={flow}
|
||||
flowMeta={flowsById.get(flow.flowId)}
|
||||
@@ -363,13 +386,23 @@ export function DashboardAnalytics({ period }: { period: UsageStatsPeriod }) {
|
||||
);
|
||||
}
|
||||
|
||||
function FlowExecutionItem({ flow, flowMeta }: { flow: FlowExecution; flowMeta?: FlowFragmentFragment }) {
|
||||
const FlowExecutionItem = memo(function FlowExecutionItem({
|
||||
flow,
|
||||
flowMeta,
|
||||
}: {
|
||||
flow: FlowExecution;
|
||||
flowMeta?: FlowFragmentFragment;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const taskCount = flow.tasks.length;
|
||||
const subtaskCount = flow.tasks.reduce((sum, task) => sum + task.subtasks.length, 0);
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
// content-visibility: auto lets the browser skip layout & paint for
|
||||
// rows outside the viewport. With ~130+ rows this is the cheapest
|
||||
// possible virtualization — no deps, no measurement, no scroll math.
|
||||
className="[content-visibility:auto] [contain-intrinsic-size:auto_56px]"
|
||||
onOpenChange={setIsOpen}
|
||||
open={isOpen}
|
||||
>
|
||||
@@ -411,9 +444,9 @@ function FlowExecutionItem({ flow, flowMeta }: { flow: FlowExecution; flowMeta?:
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function TaskExecutionItem({ task }: { task: FlowExecution['tasks'][number] }) {
|
||||
const TaskExecutionItem = memo(function TaskExecutionItem({ task }: { task: FlowExecution['tasks'][number] }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const hasSubtasks = task.subtasks.length > 0;
|
||||
|
||||
@@ -471,7 +504,7 @@ function TaskExecutionItem({ task }: { task: FlowExecution['tasks'][number] }) {
|
||||
)}
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Disables tooltip animation on chart entry and re-enables it only after the
|
||||
// tooltip has rendered at the correct position for the first time in the session.
|
||||
|
||||
Reference in New Issue
Block a user