From d4130d62c93a9f88991b47abc9d397cbe7035dd1 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Tue, 10 Mar 2026 23:23:38 +1300 Subject: [PATCH] perf: replace bare useWorkflowStore() calls with selective subscriptions WorkflowCanvas, Header, and FloatingActionBar all used useWorkflowStore() without selectors, subscribing to the entire store. Every hoveredNodeId change triggered re-renders in all three components plus a full cascade through edges and nodes from WorkflowCanvas. - WorkflowCanvas: split into individual selectors for clipboard state - Header: wrap in useShallow with explicit field selection - FloatingActionBar: wrap in useShallow with explicit field selection Co-Authored-By: Claude Opus 4.6 --- src/components/FloatingActionBar.tsx | 17 ++++++++++++++++- src/components/Header.tsx | 18 +++++++++++++++++- src/components/WorkflowCanvas.tsx | 5 ++++- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/components/FloatingActionBar.tsx b/src/components/FloatingActionBar.tsx index be230f1c..3eef6b65 100644 --- a/src/components/FloatingActionBar.tsx +++ b/src/components/FloatingActionBar.tsx @@ -2,6 +2,7 @@ import { useRef, useState, useEffect, useMemo, useCallback } from "react"; import { useWorkflowStore } from "@/store/workflowStore"; +import { useShallow } from "zustand/shallow"; import { NodeType } from "@/types"; import { useReactFlow } from "@xyflow/react"; import { ModelSearchDialog } from "./modals/ModelSearchDialog"; @@ -320,7 +321,21 @@ export function FloatingActionBar() { setModelSearchOpen, modelSearchOpen, modelSearchProvider, - } = useWorkflowStore(); + } = useWorkflowStore(useShallow((state) => ({ + nodes: state.nodes, + isRunning: state.isRunning, + currentNodeIds: state.currentNodeIds, + executeWorkflow: state.executeWorkflow, + regenerateNode: state.regenerateNode, + executeSelectedNodes: state.executeSelectedNodes, + stopWorkflow: state.stopWorkflow, + validateWorkflow: state.validateWorkflow, + edgeStyle: state.edgeStyle, + setEdgeStyle: state.setEdgeStyle, + setModelSearchOpen: state.setModelSearchOpen, + modelSearchOpen: state.modelSearchOpen, + modelSearchProvider: state.modelSearchProvider, + }))); // Get display text for running nodes const runningNodeCount = currentNodeIds.length; diff --git a/src/components/Header.tsx b/src/components/Header.tsx index c02a8f86..baf1ebda 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -2,6 +2,7 @@ import { useState, useRef, useMemo, useCallback } from "react"; import { useWorkflowStore, WorkflowFile } from "@/store/workflowStore"; +import { useShallow } from "zustand/shallow"; import { ProjectSetupModal } from "./ProjectSetupModal"; import { CostIndicator } from "./CostIndicator"; import { KeyboardShortcutsDialog } from "./KeyboardShortcutsDialog"; @@ -73,7 +74,22 @@ export function Header() { shortcutsDialogOpen, setShortcutsDialogOpen, setShowQuickstart, - } = useWorkflowStore(); + } = useWorkflowStore(useShallow((state) => ({ + workflowName: state.workflowName, + workflowId: state.workflowId, + saveDirectoryPath: state.saveDirectoryPath, + hasUnsavedChanges: state.hasUnsavedChanges, + lastSavedAt: state.lastSavedAt, + isSaving: state.isSaving, + setWorkflowMetadata: state.setWorkflowMetadata, + saveToFile: state.saveToFile, + loadWorkflow: state.loadWorkflow, + previousWorkflowSnapshot: state.previousWorkflowSnapshot, + revertToSnapshot: state.revertToSnapshot, + shortcutsDialogOpen: state.shortcutsDialogOpen, + setShortcutsDialogOpen: state.setShortcutsDialogOpen, + setShowQuickstart: state.setShowQuickstart, + }))); const [showProjectModal, setShowProjectModal] = useState(false); const [projectModalMode, setProjectModalMode] = useState<"new" | "settings">("new"); diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx index 1a5103b8..ad7d5717 100644 --- a/src/components/WorkflowCanvas.tsx +++ b/src/components/WorkflowCanvas.tsx @@ -1283,7 +1283,10 @@ export function WorkflowCanvas() { }, []); // Get copy/paste functions and clipboard from store - const { copySelectedNodes, pasteNodes, clearClipboard, clipboard } = useWorkflowStore(); + const copySelectedNodes = useWorkflowStore((state) => state.copySelectedNodes); + const pasteNodes = useWorkflowStore((state) => state.pasteNodes); + const clearClipboard = useWorkflowStore((state) => state.clearClipboard); + const clipboard = useWorkflowStore((state) => state.clipboard); // Add non-passive wheel listener to handle zoom/pan and prevent browser navigation // This replaces the onWheel prop which is passive by default and can't preventDefault