From 0e9a208cce1aa60d2dbf11394d8786ce07601107 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Fri, 16 Oct 2026 21:30:49 -0700 Subject: [PATCH] perf(web): the usage provider chart compiles without manual memoization Co-Authored-By: Claude Opus 5.5 --- .../components/usage/UsageProviderChart.tsx | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/apps/web/src/components/usage/UsageProviderChart.tsx b/apps/web/src/components/usage/UsageProviderChart.tsx index 23221fe1a8c7..4166eda94cc9 100644 --- a/apps/web/src/components/usage/UsageProviderChart.tsx +++ b/apps/web/src/components/usage/UsageProviderChart.tsx @@ -1,6 +1,6 @@ import { ProviderInstanceIcon } from "../chat/ProviderInstanceIcon"; import type { UsageProviderKind } from "@t3tools/contracts"; -import { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { useLayoutEffect, useRef, useState } from "react"; import type { DailyTotals, HourlyTotals } from "@t3tools/shared/usageMerge"; import { @@ -223,6 +223,21 @@ function buildChart( return { columns, scale, stepX, paths: paths.toSorted((a, b) => b.total - a.total) }; } +// Rebuilding the chart walks every period and provider; keep it cached across hover renders. +function useChart( + periods: readonly string[], + totals: readonly DailyTotals[] | readonly HourlyTotals[], + metric: UsageChartMetric, + providers: readonly UsageProviderKind[], + loadingProviders: ReadonlySet, +) { + "use memo"; + const byPeriod = new Map( + totals.map((entry) => ["hourStart" in entry ? entry.hourStart : entry.day, entry]), + ); + return buildChart(periods, byPeriod, metric, providers, loadingProviders); +} + export function UsageProviderChart({ providers, loadingProviders = NONE_LOADING, @@ -236,29 +251,25 @@ export function UsageProviderChart({ timeZone, }: UsageProviderChartProps) { const periods = resolution === "hour" ? hours : days; - const byPeriod = useMemo( - () => - resolution === "hour" - ? new Map(hourly.map((entry) => [entry.hourStart, entry])) - : new Map(daily.map((entry) => [entry.day, entry])), - [daily, hourly, resolution], + const { columns, scale, paths, stepX } = useChart( + periods, + resolution === "hour" ? hourly : daily, + metric, + providers, + loadingProviders, ); const [hoverIndex, setHoverIndex] = useState(null); const plotRef = useRef(null); const tooltipRef = useRef(null); const hoverPositionRef = useRef<{ x: number; y: number } | null>(null); - const { columns, scale, paths, stepX } = useMemo( - () => buildChart(periods, byPeriod, metric, providers, loadingProviders), - [byPeriod, loadingProviders, metric, periods, providers], - ); const toY = (value: number) => valueToY(value, scale.max); const seriesClassName = (loading: boolean) => cn("transition-opacity", loading && "opacity-40 delay-150"); const format = metric === "tokens" ? formatTokens : formatUsd; - const positionTooltip = useCallback(() => { + const positionTooltip = () => { const plot = plotRef.current; const tooltip = tooltipRef.current; const hoverPosition = hoverPositionRef.current; @@ -281,7 +292,7 @@ export function UsageProviderChart({ const top = Math.min(Math.max(0, preferredTop), Math.max(0, plotHeight - tooltipHeight)); plot.style.setProperty("--usage-tooltip-left", `${left}px`); plot.style.setProperty("--usage-tooltip-top", `${top}px`); - }, []); + }; useLayoutEffect(() => { if (hoverIndex === null) return; @@ -294,22 +305,19 @@ export function UsageProviderChart({ return observeResize([plot, tooltip], positionTooltip); }, [hoverIndex, positionTooltip]); - const handleMove = useCallback( - (event: React.MouseEvent) => { - const plot = plotRef.current; - if (plot === null || periods.length === 0) return; - const bounds = plot.getBoundingClientRect(); - if (bounds.width === 0) return; - const localX = Math.min(bounds.width, Math.max(0, event.clientX - bounds.left)); - const localY = Math.min(bounds.height, Math.max(0, event.clientY - bounds.top)); - const fraction = localX / bounds.width; - const index = Math.round(fraction * (periods.length - 1)); - hoverPositionRef.current = { x: localX, y: localY }; - positionTooltip(); - setHoverIndex(Math.min(periods.length - 1, Math.max(0, index))); - }, - [periods.length, positionTooltip], - ); + const handleMove = (event: React.MouseEvent) => { + const plot = plotRef.current; + if (plot === null || periods.length === 0) return; + const bounds = plot.getBoundingClientRect(); + if (bounds.width === 0) return; + const localX = Math.min(bounds.width, Math.max(0, event.clientX - bounds.left)); + const localY = Math.min(bounds.height, Math.max(0, event.clientY - bounds.top)); + const fraction = localX / bounds.width; + const index = Math.round(fraction * (periods.length - 1)); + hoverPositionRef.current = { x: localX, y: localY }; + positionTooltip(); + setHoverIndex(Math.min(periods.length - 1, Math.max(0, index))); + }; const hoveredPeriod = hoverIndex === null ? undefined : periods[hoverIndex]; const hoveredColumn = hoverIndex === null ? undefined : columns[hoverIndex];