From 5c3ab567259ff88086c37df56e910c1d83640641 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Tue, 10 Mar 2026 23:09:35 +1300 Subject: [PATCH] perf: RAF-debounce setHoveredNodeId and skip hover during panning Rapid mouse movement across the canvas fires multiple mouseout events per frame, each triggering ~150 Zustand selector evaluations. This coalesces hover updates into one per animation frame and skips them entirely during canvas panning/dragging. Co-Authored-By: Claude Opus 4.6 --- src/components/nodes/BaseNode.tsx | 10 ++++++++-- src/store/workflowStore.ts | 10 +++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/components/nodes/BaseNode.tsx b/src/components/nodes/BaseNode.tsx index ba44b363..fb5cd96a 100644 --- a/src/components/nodes/BaseNode.tsx +++ b/src/components/nodes/BaseNode.tsx @@ -205,8 +205,14 @@ export function BaseNode({ ${!fullBleed && selected && settingsExpanded ? "border-blue-500" : ""} ${className} `} - onMouseEnter={() => setHoveredNodeId(id)} - onMouseLeave={() => setHoveredNodeId(null)} + onMouseEnter={() => { + if (document.querySelector('.react-flow__pane.dragging')) return; + setHoveredNodeId(id); + }} + onMouseLeave={() => { + if (document.querySelector('.react-flow__pane.dragging')) return; + setHoveredNodeId(null); + }} >
{children}
diff --git a/src/store/workflowStore.ts b/src/store/workflowStore.ts index 472206ab..c0a74cc9 100644 --- a/src/store/workflowStore.ts +++ b/src/store/workflowStore.ts @@ -382,6 +382,10 @@ let nodeIdCounter = 0; let groupIdCounter = 0; let autoSaveIntervalId: ReturnType | null = null; +// RAF debounce for hover updates — coalesces rapid mouseenter/mouseleave events +// into a single store update per animation frame +let hoverRafId: number | null = null; + // Track pending save-generation syncs to ensure IDs are resolved before workflow save const pendingImageSyncs = new Map>(); @@ -526,7 +530,11 @@ const workflowStoreImpl: StateCreator = (set, get) => ({ }, setHoveredNodeId: (id: string | null) => { - set({ hoveredNodeId: id }); + if (hoverRafId !== null) cancelAnimationFrame(hoverRafId); + hoverRafId = requestAnimationFrame(() => { + hoverRafId = null; + if (get().hoveredNodeId !== id) set({ hoveredNodeId: id }); + }); }, addNode: (type: NodeType, position: XYPosition, initialData?: Partial) => {