Browse Source

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 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 4 months ago
parent
commit
5c3ab56725
  1. 10
      src/components/nodes/BaseNode.tsx
  2. 10
      src/store/workflowStore.ts

10
src/components/nodes/BaseNode.tsx

@ -205,8 +205,14 @@ export function BaseNode({
${!fullBleed && selected && settingsExpanded ? "border-blue-500" : ""} ${!fullBleed && selected && settingsExpanded ? "border-blue-500" : ""}
${className} ${className}
`} `}
onMouseEnter={() => setHoveredNodeId(id)} onMouseEnter={() => {
onMouseLeave={() => setHoveredNodeId(null)} if (document.querySelector('.react-flow__pane.dragging')) return;
setHoveredNodeId(id);
}}
onMouseLeave={() => {
if (document.querySelector('.react-flow__pane.dragging')) return;
setHoveredNodeId(null);
}}
> >
<div className={contentClassName ?? (fullBleed ? "flex-1 min-h-0 relative" : "px-3 pb-4 flex-1 min-h-0 overflow-hidden flex flex-col")}>{children}</div> <div className={contentClassName ?? (fullBleed ? "flex-1 min-h-0 relative" : "px-3 pb-4 flex-1 min-h-0 overflow-hidden flex flex-col")}>{children}</div>
</div> </div>

10
src/store/workflowStore.ts

@ -382,6 +382,10 @@ let nodeIdCounter = 0;
let groupIdCounter = 0; let groupIdCounter = 0;
let autoSaveIntervalId: ReturnType<typeof setInterval> | null = null; let autoSaveIntervalId: ReturnType<typeof setInterval> | 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 // Track pending save-generation syncs to ensure IDs are resolved before workflow save
const pendingImageSyncs = new Map<string, Promise<void>>(); const pendingImageSyncs = new Map<string, Promise<void>>();
@ -526,7 +530,11 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (set, get) => ({
}, },
setHoveredNodeId: (id: string | null) => { 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<WorkflowNodeData>) => { addNode: (type: NodeType, position: XYPosition, initialData?: Partial<WorkflowNodeData>) => {

Loading…
Cancel
Save