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" : ""}
${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);
}}
>
<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>

10
src/store/workflowStore.ts

@ -382,6 +382,10 @@ let nodeIdCounter = 0;
let groupIdCounter = 0;
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
const pendingImageSyncs = new Map<string, Promise<void>>();
@ -526,7 +530,11 @@ const workflowStoreImpl: StateCreator<WorkflowStore> = (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<WorkflowNodeData>) => {

Loading…
Cancel
Save