From ce58673d187e5a51b74167fb2910bff29fc1070a Mon Sep 17 00:00:00 2001 From: dtoro Date: Sat, 14 Mar 2026 22:39:38 +0100 Subject: [PATCH] fix: performance --- frontend/src/app/canvas/CanvasPage.tsx | 32 +++++++++++++++++-- .../nodes/render/useRenderingNodeState.ts | 17 ++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/canvas/CanvasPage.tsx b/frontend/src/app/canvas/CanvasPage.tsx index cadc500..2cd8661 100644 --- a/frontend/src/app/canvas/CanvasPage.tsx +++ b/frontend/src/app/canvas/CanvasPage.tsx @@ -188,8 +188,21 @@ export function CanvasPage({ projectId }: CanvasPageProps) { const [isSelecting, setIsSelecting] = React.useState(false) const [ariaAnnouncement, setAriaAnnouncement] = React.useState(null) const [fullscreenNodeId, setFullscreenNodeId] = React.useState(null) + + const graphApplyTimeoutRef = React.useRef | null>(null) + const GRAPH_APPLY_DEBOUNCE_MS = 300 useEffect(() => { - dispatchCanvasCommand({ type: 'graph/apply', payload: { nodes, edges } }) + if (graphApplyTimeoutRef.current) clearTimeout(graphApplyTimeoutRef.current) + graphApplyTimeoutRef.current = setTimeout(() => { + graphApplyTimeoutRef.current = null + dispatchCanvasCommand({ type: 'graph/apply', payload: { nodes, edges } }) + }, GRAPH_APPLY_DEBOUNCE_MS) + return () => { + if (graphApplyTimeoutRef.current) { + clearTimeout(graphApplyTimeoutRef.current) + graphApplyTimeoutRef.current = null + } + } }, [nodes, edges]) const connectionPath = useCanvasConnectionPathFromStore() @@ -218,7 +231,22 @@ export function CanvasPage({ projectId }: CanvasPageProps) { rafRef.current = requestAnimationFrame(() => { rafRef.current = null const toApply = pendingChangesRef.current.splice(0, pendingChangesRef.current.length) - if (toApply.length > 0) setNodesSilent((nds) => applyNodeChanges(toApply, nds)) + if (toApply.length === 0) return + setNodesSilent((nds) => { + // Drop dimension-only changes that don't change the node (e.g. React Flow re-reporting on visibility). + // Avoids graph/apply and store churn when nodes become visible with onlyRenderVisibleElements. + const filtered = toApply.filter((c) => { + const ch = c as NodeChange & { type?: string; dimensions?: { width?: number; height?: number } } + if (ch.type !== 'dimensions' || ch.dimensions == null) return true + const node = nds.find((n) => n.id === (ch as { id?: string }).id) + if (!node) return true + const nw = (node as Node & { width?: number }).width + const nh = (node as Node & { height?: number }).height + return nw !== ch.dimensions.width || nh !== ch.dimensions.height + }) + if (filtered.length === 0) return nds + return applyNodeChanges(filtered, nds) + }) }) } }, diff --git a/frontend/src/components/nodes/render/useRenderingNodeState.ts b/frontend/src/components/nodes/render/useRenderingNodeState.ts index 1b61bbe..68116c9 100644 --- a/frontend/src/components/nodes/render/useRenderingNodeState.ts +++ b/frontend/src/components/nodes/render/useRenderingNodeState.ts @@ -38,6 +38,8 @@ export type RenderingNodeData = { const DEFAULT_VIEWPORT_WIDTH = 1200 const DEFAULT_VIEWPORT_HEIGHT = 800 const RENDER_DEBOUNCE_MS = 250 +/** Skip auto-run when we have cache and effect runs soon after mount (node became visible). */ +const VISIBILITY_GRACE_MS = 400 /** Lifecycle state to pass to useSyncConnectionStatus so edge status (updating/error) stays in sync. */ export type RenderingNodeLifecycle = { @@ -194,6 +196,8 @@ export function useRenderingNodeState( const minLoadingTimeoutRef = useRef | null>(null) const lastManualRunTriggerRef = useRef(0) const manualRunTriggerSyncedRef = useRef(false) + /** In auto mode, skip running when we have cache and we're in the grace window after mount (node became visible). */ + const mountTimeRef = useRef(0) const updateDataRef = useRef(updateData) updateDataRef.current = updateData @@ -214,6 +218,7 @@ export function useRenderingNodeState( useSyncConnectionStatus(id, lifecycle) useEffect(() => { + if (mountTimeRef.current === 0) mountTimeRef.current = Date.now() if (incomingIds.length === 0) { setRenderedContent(null) setResolvedContent(null) @@ -258,6 +263,18 @@ export function useRenderingNodeState( lastManualRunTriggerRef.current = runTrigger } + // Auto mode: do not run when node just became visible (remount with cache). No pipeline run, no path/addTrigger. + if (effectiveUpdateMode === 'auto') { + const hasCache = Boolean( + (data?.cachedRenderedContent ?? data?.cachedResolvedContent) as string | undefined + ) + const signatureUnchanged = + data?.lastRunSourceSignature != null && data.lastRunSourceSignature === sourceSignature + const withinVisibilityGrace = + mountTimeRef.current > 0 && Date.now() - mountTimeRef.current < VISIBILITY_GRACE_MS + if (hasCache && (signatureUnchanged || withinVisibilityGrace)) return + } + runIdRef.current += 1 const thisRunId = runIdRef.current const signatureForThisRun = sourceSignature