From c19ed30d6d2faf6cb5d84390d25a7a0503c2a47f Mon Sep 17 00:00:00 2001 From: ogabrielluiz Date: Tue, 12 May 2026 11:04:16 -0300 Subject: [PATCH] perf(flow): avoid PageComponent re-renders on helper-line mousemove onNodeDrag stored helperLines in local PageComponent state and depended on the nodes array, so every drag tick re-rendered the whole canvas and recreated the callback. That surfaced as 300ms+ 'mousemove' handler violations while dragging. Move helperLines into a dedicated tiny store consumed only by the HelperLines overlay, and coalesce per-mousemove computation into one requestAnimationFrame that reads the latest nodes from the flow store at compute time. The drag callback no longer depends on nodes. --- .../PageComponent/components/helper-lines.tsx | 9 ++-- .../components/PageComponent/index.tsx | 43 +++++++++++++------ src/frontend/src/stores/helperLinesStore.ts | 14 ++++++ 3 files changed, 47 insertions(+), 19 deletions(-) create mode 100644 src/frontend/src/stores/helperLinesStore.ts diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/components/helper-lines.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/components/helper-lines.tsx index 2464d66fb2..952f503bc0 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/components/helper-lines.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/components/helper-lines.tsx @@ -1,11 +1,8 @@ import { useViewport } from "@xyflow/react"; -import { HelperLinesState } from "../helpers/helper-lines"; +import { useHelperLinesStore } from "@/stores/helperLinesStore"; -interface HelperLinesProps { - helperLines: HelperLinesState; -} - -export default function HelperLines({ helperLines }: HelperLinesProps) { +export default function HelperLines() { + const helperLines = useHelperLinesStore((state) => state.helperLines); const { x: viewportX, y: viewportY, zoom } = useViewport(); if (!helperLines.horizontal && !helperLines.vertical) { diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index d1b28cbf92..64cce12c41 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -18,6 +18,7 @@ import { useState, } from "react"; import { useHotkeys } from "react-hotkeys-hook"; +import { useTranslation } from "react-i18next"; import { useShallow } from "zustand/react/shallow"; import ForwardedIconComponent from "@/components/common/genericIconComponent"; import FlowToolbar from "@/components/core/flowToolbarComponent"; @@ -33,18 +34,17 @@ import CustomLoader from "@/customization/components/custom-loader"; import { track } from "@/customization/utils/analytics"; import useApplyFlowToCanvas from "@/hooks/flows/use-apply-flow-to-canvas"; import useAutoSaveFlow from "@/hooks/flows/use-autosave-flow"; - import { useFlowEvents } from "@/hooks/flows/use-flow-events"; import useUploadFlow from "@/hooks/flows/use-upload-flow"; import { useAddComponent } from "@/hooks/use-add-component"; import InspectionPanel from "@/pages/FlowPage/components/InspectionPanel"; import { nodeColorsName } from "@/utils/styleUtils"; import { isSupportedNodeTypes } from "@/utils/utils"; -import { useTranslation } from "react-i18next"; import ExportModal from "../../../../modals/exportModal"; import useAlertStore from "../../../../stores/alertStore"; import useFlowStore from "../../../../stores/flowStore"; import useFlowsManagerStore from "../../../../stores/flowsManagerStore"; +import { useHelperLinesStore } from "../../../../stores/helperLinesStore"; import { useShortcutsStore } from "../../../../stores/shortcuts"; import { useTypesStore } from "../../../../stores/typesStore"; import useVersionPreviewStore from "../../../../stores/versionPreviewStore"; @@ -72,11 +72,7 @@ import UpdateAllComponents from "../UpdateAllComponents"; import { CanvasBadge } from "./components/CanvasBanner"; import HelperLines from "./components/helper-lines"; import VersionPreviewOverlay from "./components/VersionPreviewOverlay"; -import { - getHelperLines, - getSnapPosition, - type HelperLinesState, -} from "./helpers/helper-lines"; +import { getHelperLines, getSnapPosition } from "./helpers/helper-lines"; import { MemoizedBackground, MemoizedCanvasControls, @@ -531,18 +527,36 @@ export default function Page({ [takeSnapshot, onConnect], ); - const [helperLines, setHelperLines] = useState({}); const [isDragging, setIsDragging] = useState(false); const helperLineEnabled = useFlowStore((state) => state.helperLineEnabled); + const setHelperLines = useHelperLinesStore((state) => state.setHelperLines); + const helperLineRafRef = useRef(null); + + const cancelHelperLineFrame = useCallback(() => { + if (helperLineRafRef.current !== null) { + cancelAnimationFrame(helperLineRafRef.current); + helperLineRafRef.current = null; + } + }, []); + + useEffect(() => cancelHelperLineFrame, [cancelHelperLineFrame]); const onNodeDrag: OnNodeDrag = useCallback( (_, node) => { - if (helperLineEnabled) { - const currentHelperLines = getHelperLines(node, nodes); - setHelperLines(currentHelperLines); + if (!helperLineEnabled) return; + // Coalesce per-mousemove work into one frame and read the latest nodes + // from the store at compute time so this callback doesn't get recreated + // (and the canvas doesn't re-render) on every node change. + if (helperLineRafRef.current !== null) { + cancelAnimationFrame(helperLineRafRef.current); } + helperLineRafRef.current = requestAnimationFrame(() => { + helperLineRafRef.current = null; + const latestNodes = useFlowStore.getState().nodes; + setHelperLines(getHelperLines(node, latestNodes)); + }); }, - [helperLineEnabled, nodes], + [helperLineEnabled, setHelperLines], ); const onNodeDragStart: OnNodeDrag = useCallback( @@ -562,6 +576,7 @@ export default function Page({ updateCurrentFlow({ nodes }); setPositionDictionary({}); setIsDragging(false); + cancelHelperLineFrame(); setHelperLines({}); }, [ @@ -571,6 +586,8 @@ export default function Page({ edges, reactFlowInstance, setPositionDictionary, + cancelHelperLineFrame, + setHelperLines, ], ); @@ -1010,7 +1027,7 @@ export default function Page({ > - {helperLineEnabled && } + {helperLineEnabled && } {bannerVisible && ( diff --git a/src/frontend/src/stores/helperLinesStore.ts b/src/frontend/src/stores/helperLinesStore.ts new file mode 100644 index 0000000000..64ac5f33e6 --- /dev/null +++ b/src/frontend/src/stores/helperLinesStore.ts @@ -0,0 +1,14 @@ +import { create } from "zustand"; +import type { HelperLinesState } from "@/pages/FlowPage/components/PageComponent/helpers/helper-lines"; + +type HelperLinesStore = { + helperLines: HelperLinesState; + setHelperLines: (helperLines: HelperLinesState) => void; +}; + +export const useHelperLinesStore = create((set) => ({ + helperLines: {}, + setHelperLines: (helperLines) => set({ helperLines }), +})); + +export default useHelperLinesStore;