From 977ba926c624dd161c7de33a4197eafff3fa64ee Mon Sep 17 00:00:00 2001 From: Cristhian Zanforlin Lousa Date: Fri, 13 Dec 2024 19:24:45 -0300 Subject: [PATCH] perf: Optimize component rendering with memoization and useCallback hooks (#5253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ (NodeDescription/index.tsx): Memoize Markdown component and description rendering for performance optimization ♻️ (NodeOutputfield/index.tsx): Memoize HandleRenderComponent and Handle instance with useMemo and memo for performance optimization ✨ (RenderInputParameters/index.tsx): Add new component RenderInputParameters to render input parameters with memoization for improved performance 📝 (GenericNode/index.tsx): Remove unused imports and functions, refactor sortToolModeFields to be exported, and optimize key generation and memoization for NodeOutputField component 📝 (flowSidebarComponent/index.tsx): Refactor event handlers to use useCallback for better performance 📝 (nodeToolbarComponent/index.tsx): Refactor event handlers to use useCallback for better performance and readability * ♻️ (NodeDescription/index.tsx): Remove unnecessary comments and improve code readability by removing redundant comments ♻️ (NodeOutputfield/index.tsx): Remove unnecessary comments and improve code readability by removing redundant comments ♻️ (RenderInputParameters/index.tsx): Remove unnecessary comments and improve code readability by removing redundant comments ♻️ (index.tsx): Remove unnecessary comments and improve code readability by removing redundant comments * ✅ (decisionFlow.spec.ts): add @workflow tag to the test case for better categorization and organization. * 📝 (GenericNode/index.tsx): Organize imports and update component import for better code structure and readability 📝 (GenericNode/index.tsx): Refactor renderOutputs function to improve code readability and maintainability 📝 (GenericNode/index.tsx): Refactor renderOutputParameter function to use OutputParameter component for consistency 📝 (GenericNode/index.tsx): Refactor output rendering logic to use OutputParameter component for better code structure 📝 (sidebarDraggableComponent/index.tsx): Update logic to remove cursor element only if it exists to prevent errors * ✨ (NodeOutputParameter/index.tsx): Add a new component for rendering output parameters in the frontend to improve modularity and reusability. --- .../components/NodeDescription/index.tsx | 35 +-- .../components/NodeOutputParameter/index.tsx | 56 +++++ .../components/NodeOutputfield/index.tsx | 64 ++++-- .../RenderInputParameters/index.tsx | 122 +++++++++++ .../src/CustomNodes/GenericNode/index.tsx | 203 ++++++------------ .../sidebarDraggableComponent/index.tsx | 10 +- .../components/flowSidebarComponent/index.tsx | 29 ++- .../components/nodeToolbarComponent/index.tsx | 51 ++--- .../core/integrations/decisionFlow.spec.ts | 2 +- 9 files changed, 372 insertions(+), 200 deletions(-) create mode 100644 src/frontend/src/CustomNodes/GenericNode/components/NodeOutputParameter/index.tsx create mode 100644 src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx diff --git a/src/frontend/src/CustomNodes/GenericNode/components/NodeDescription/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/NodeDescription/index.tsx index 6fdc7cb20..450b64381 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/NodeDescription/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/NodeDescription/index.tsx @@ -3,7 +3,7 @@ import useFlowsManagerStore from "@/stores/flowsManagerStore"; import useFlowStore from "@/stores/flowStore"; import { handleKeyDown } from "@/utils/reactflowUtils"; import { cn } from "@/utils/utils"; -import { useEffect, useRef, useState } from "react"; +import { memo, useEffect, useMemo, useRef, useState } from "react"; import Markdown from "react-markdown"; export default function NodeDescription({ @@ -59,6 +59,25 @@ export default function NodeDescription({ setNodeDescription(description); }, [description]); + const MemoizedMarkdown = memo(Markdown); + const renderedDescription = useMemo( + () => + description === "" || !description ? ( + emptyPlaceholder + ) : ( + + {String(description)} + + ), + [description, emptyPlaceholder, mdClassName], + ); + return (
- {description === "" || !description ? ( - emptyPlaceholder - ) : ( - - {String(description)} - - )} + {renderedDescription}
)} diff --git a/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputParameter/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputParameter/index.tsx new file mode 100644 index 000000000..9bf3c4f9e --- /dev/null +++ b/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputParameter/index.tsx @@ -0,0 +1,56 @@ +import React, { useMemo } from "react"; + +import { getNodeOutputColors } from "../../../helpers/get-node-output-colors"; +import { getNodeOutputColorsName } from "../../../helpers/get-node-output-colors-name"; +import NodeOutputField from "../NodeOutputfield"; + +export const OutputParameter = ({ + output, + idx, + lastOutput, + data, + types, + selected, + showNode, + isToolMode, +}) => { + const id = useMemo( + () => ({ + output_types: [output.selected ?? output.types[0]], + id: data.id, + dataType: data.type, + name: output.name, + }), + [output.selected, output.types, data.id, data.type, output.name], + ); + + const colors = useMemo( + () => getNodeOutputColors(output, data, types), + [output, data.type, data.id, types], + ); + + const colorNames = useMemo( + () => getNodeOutputColorsName(output, data, types), + [output, data.type, data.id, types], + ); + + return ( + + ); +}; diff --git a/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx index c08bea5e9..8047dc467 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx @@ -1,6 +1,6 @@ import { ICON_STROKE_WIDTH } from "@/constants/constants"; import { cloneDeep } from "lodash"; -import { useEffect, useRef } from "react"; +import { memo, useEffect, useMemo, useRef } from "react"; import { useUpdateNodeInternals } from "reactflow"; import { default as IconComponent } from "../../../../components/common/genericIconComponent"; import ShadTooltip from "../../../../components/common/shadTooltipComponent"; @@ -105,22 +105,52 @@ export default function NodeOutputField({ } }, [disabledOutput]); - const Handle = ( - + const MemoizedHandleRenderComponent = memo( + HandleRenderComponent, + (prev, next) => { + return ( + prev.nodeId === next.nodeId && + prev.myData === next.myData && + prev.showNode === next.showNode && + prev.tooltipTitle === next.tooltipTitle && + prev.colors === next.colors && + prev.colorName === next.colorName + ); + }, + ); + + const Handle = useMemo( + () => ( + + ), + [ + nodes, + tooltipTitle, + id, + title, + edges, + data.id, + myData, + colors, + setFilterEdge, + showNode, + data?.type, + colorName, + ], ); return !showNode ? ( diff --git a/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx new file mode 100644 index 000000000..7af939e2e --- /dev/null +++ b/src/frontend/src/CustomNodes/GenericNode/components/RenderInputParameters/index.tsx @@ -0,0 +1,122 @@ +import { getNodeInputColors } from "@/CustomNodes/helpers/get-node-input-colors"; +import { getNodeInputColorsName } from "@/CustomNodes/helpers/get-node-input-colors-name"; +import getFieldTitle from "@/CustomNodes/utils/get-field-title"; +import { scapedJSONStringfy } from "@/utils/reactflowUtils"; +import { useMemo } from "react"; +import { sortToolModeFields } from "../.."; +import NodeInputField from "../NodeInputField"; + +const RenderInputParameters = ({ + data, + types, + isToolMode, + showNode, + shownOutputs, + showHiddenOutputs, +}) => { + const templateFields = useMemo(() => { + return Object.keys(data.node?.template || {}) + .filter((templateField) => templateField.charAt(0) !== "_") + .sort((a, b) => + sortToolModeFields( + a, + b, + data.node!.template, + data.node?.field_order ?? [], + isToolMode, + ), + ); + }, [data.node?.template, data.node?.field_order, isToolMode]); + + const memoizedColors = useMemo(() => { + const colorMap = new Map(); + + templateFields.forEach((templateField) => { + const template = data.node?.template[templateField]; + if (template) { + colorMap.set(templateField, { + colors: getNodeInputColors( + template.input_types, + template.type, + types, + ), + colorsName: getNodeInputColorsName( + template.input_types, + template.type, + types, + ), + }); + } + }); + + return colorMap; + }, [templateFields, types, data.node?.template]); + + const memoizedKeys = useMemo(() => { + const keyMap = new Map(); + + templateFields.forEach((templateField) => { + const template = data.node?.template[templateField]; + if (template) { + keyMap.set( + templateField, + scapedJSONStringfy({ + inputTypes: template.input_types, + type: template.type, + id: data.id, + fieldName: templateField, + proxy: template.proxy, + }), + ); + } + }); + + return keyMap; + }, [templateFields, data.id, data.node?.template]); + + const renderInputParameter = templateFields.map( + (templateField: string, idx) => { + const template = data.node?.template[templateField]; + + if (!template?.show || template?.advanced) { + return null; + } + + const memoizedColor = memoizedColors.get(templateField); + const memoizedKey = memoizedKeys.get(templateField); + + return ( + 0 || showHiddenOutputs) + } + key={memoizedKey} + data={data} + colors={memoizedColor.colors} + title={getFieldTitle(data.node?.template!, templateField)} + info={template.info!} + name={templateField} + tooltipTitle={template.input_types?.join("\n") ?? template.type} + required={template.required} + id={{ + inputTypes: template.input_types, + type: template.type, + id: data.id, + fieldName: templateField, + }} + type={template.type} + optionalHandle={template.input_types} + proxy={template.proxy} + showNode={showNode} + colorName={memoizedColor.colorsName} + isToolMode={isToolMode && template.tool_mode} + /> + ); + }, + ); + + return <>{renderInputParameter}; +}; + +export default RenderInputParameters; diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index b8460ad5a..84e117a34 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -17,29 +17,22 @@ import { useShortcutsStore } from "../../stores/shortcuts"; import { useTypesStore } from "../../stores/typesStore"; import { OutputFieldType, VertexBuildTypeAPI } from "../../types/api"; import { NodeDataType } from "../../types/flow"; -import { - checkHasToolMode, - scapedJSONStringfy, -} from "../../utils/reactflowUtils"; +import { checkHasToolMode } from "../../utils/reactflowUtils"; import { classNames, cn } from "../../utils/utils"; -import { getNodeInputColors } from "../helpers/get-node-input-colors"; -import { getNodeInputColorsName } from "../helpers/get-node-input-colors-name"; -import { getNodeOutputColors } from "../helpers/get-node-output-colors"; -import { getNodeOutputColorsName } from "../helpers/get-node-output-colors-name"; + import { processNodeAdvancedFields } from "../helpers/process-node-advanced-fields"; import useCheckCodeValidity from "../hooks/use-check-code-validity"; import useUpdateNodeCode from "../hooks/use-update-node-code"; -import getFieldTitle from "../utils/get-field-title"; import sortFields from "../utils/sort-fields"; import NodeDescription from "./components/NodeDescription"; -import NodeInputField from "./components/NodeInputField"; import NodeName from "./components/NodeName"; -import NodeOutputField from "./components/NodeOutputfield"; +import { OutputParameter } from "./components/NodeOutputParameter"; import NodeStatus from "./components/NodeStatus"; +import RenderInputParameters from "./components/RenderInputParameters"; import { NodeIcon } from "./components/nodeIcon"; import { useBuildStatus } from "./hooks/use-get-build-status"; -const sortToolModeFields = ( +export const sortToolModeFields = ( a: string, b: string, template: any, @@ -172,42 +165,23 @@ export default function GenericNode({ const [openShowMoreOptions, setOpenShowMoreOptions] = useState(false); - const renderOutputParameter = ( - output: OutputFieldType, - idx: number, - lastOutput: boolean, - ) => { - return ( - { + return outputs.map((output, idx) => ( + out.name === output.name) ?? + idx } + lastOutput={idx === outputs.length - 1} data={data} - colors={getNodeOutputColors(output, data, types)} - outputProxy={output.proxy} - title={output.display_name ?? output.name} - tooltipTitle={output.selected ?? output.types[0]} - id={{ - output_types: [output.selected ?? output.types[0]], - id: data.id, - dataType: data.type, - name: output.name, - }} - type={output.types.join("|")} + types={types} + selected={selected} showNode={showNode} - outputName={output.name} - colorName={getNodeOutputColorsName(output, data, types)} isToolMode={isToolMode} /> - ); + )); }; useEffect(() => { @@ -260,72 +234,6 @@ export default function GenericNode({ data.node?.outputs?.some((output) => output.name === "component_as_tool") ?? false; - const renderInputParameter = Object.keys(data.node!.template) - .filter((templateField) => templateField.charAt(0) !== "_") - .sort((a, b) => - sortToolModeFields( - a, - b, - data.node!.template, - data.node?.field_order ?? [], - isToolMode, - ), - ) - .map( - (templateField: string, idx) => - data.node!.template[templateField]?.show && - !data.node!.template[templateField]?.advanced && ( - templateField.charAt(0) !== "_", - ).length - - 1 && !(shownOutputs.length > 0 || showHiddenOutputs) - } - key={scapedJSONStringfy({ - inputTypes: data.node!.template[templateField].input_types, - type: data.node!.template[templateField].type, - id: data.id, - fieldName: templateField, - proxy: data.node!.template[templateField].proxy, - })} - data={data} - colors={getNodeInputColors( - data.node?.template[templateField].input_types, - data.node?.template[templateField].type, - types, - )} - title={getFieldTitle(data.node?.template!, templateField)} - info={data.node?.template[templateField].info!} - name={templateField} - tooltipTitle={ - data.node?.template[templateField].input_types?.join("\n") ?? - data.node?.template[templateField].type - } - required={data.node!.template[templateField].required} - id={{ - inputTypes: data.node!.template[templateField].input_types, - type: data.node!.template[templateField].type, - id: data.id, - fieldName: templateField, - }} - type={data.node?.template[templateField].type} - optionalHandle={data.node?.template[templateField].input_types} - proxy={data.node?.template[templateField].proxy} - showNode={showNode} - colorName={getNodeInputColorsName( - data.node?.template[templateField].input_types, - data.node?.template[templateField].type, - types, - )} - isToolMode={ - isToolMode && data.node!.template[templateField].tool_mode - } - /> - ), - ); - const buildStatus = useBuildStatus(data, data.id); const hasOutputs = data.node?.outputs && data.node?.outputs.length > 0; const [validationStatus, setValidationStatus] = @@ -412,16 +320,17 @@ export default function GenericNode({
{!showNode && ( <> - {renderInputParameter} + {shownOutputs && shownOutputs.length > 0 && - renderOutputParameter( - shownOutputs[0], - data.node!.outputs?.findIndex( - (out) => out.name === shownOutputs[0].name, - ) ?? 0, - false, - )} + renderOutputs(shownOutputs)} )}
@@ -452,10 +361,15 @@ export default function GenericNode({ {showNode && (
- {/* increase height!! */} - <> - {renderInputParameter} +
{!showHiddenOutputs && shownOutputs && - shownOutputs.map((output, idx) => - renderOutputParameter( - output, - data.node!.outputs?.findIndex( - (out) => out.name === output.name, - ) ?? idx, - idx === shownOutputs.length - 1, - ), - )} + shownOutputs.map((output, idx) => ( + out.name === output.name, + ) ?? idx + } + lastOutput={idx === shownOutputs.length - 1} + data={data} + types={types} + selected={selected} + showNode={showNode} + isToolMode={isToolMode} + /> + ))}
- {data.node!.outputs && - data.node!.outputs.map((output, idx) => { - return renderOutputParameter( - output, + {data.node!.outputs?.map((output, idx) => ( + out.name === output.name, - ) ?? idx, - idx === (data.node!.outputs?.length ?? 0) - 1, - ); - })} + ) ?? idx + } + lastOutput={idx === (data.node!.outputs?.length ?? 0) - 1} + data={data} + types={types} + selected={selected} + showNode={showNode} + isToolMode={isToolMode} + /> + ))}
{hiddenOutputs && hiddenOutputs.length > 0 && ( diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarDraggableComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarDraggableComponent/index.tsx index cdce9a7c3..25af5b692 100644 --- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarDraggableComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarDraggableComponent/index.tsx @@ -140,9 +140,13 @@ export const SidebarDraggableComponent = forwardRef( }} onDragStart={onDragStart} onDragEnd={() => { - document.body.removeChild( - document.getElementsByClassName("cursor-grabbing")[0], - ); + if ( + document.getElementsByClassName("cursor-grabbing").length > 0 + ) { + document.body.removeChild( + document.getElementsByClassName("cursor-grabbing")[0], + ); + } }} > state.nodes); const chatInputAdded = checkChatInput(nodes); + const handleInputFocus = useCallback( + (event: React.FocusEvent) => { + setIsInputFocused(true); + }, + [], + ); + + const handleInputBlur = useCallback( + (event: React.FocusEvent) => { + setIsInputFocused(false); + }, + [], + ); + + const handleInputChange = useCallback( + (event: React.ChangeEvent) => { + handleSearchInput(event.target.value); + }, + [], + ); + return ( setIsInputFocused(true)} - onBlur={() => setIsInputFocused(false)} + onFocus={handleInputFocus} + onBlur={handleInputBlur} + onChange={handleInputChange} value={search} - onChange={(e) => handleSearchInput(e.target.value)} /> {!isInputFocused && search === "" && (
diff --git a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx index 5eeef286c..3be8f759c 100644 --- a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx @@ -12,7 +12,7 @@ import useAddFlow from "@/hooks/flows/use-add-flow"; import CodeAreaModal from "@/modals/codeAreaModal"; import { APIClassType } from "@/types/api"; import _, { cloneDeep } from "lodash"; -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useUpdateNodeInternals } from "reactflow"; import IconComponent from "../../../../components/common/genericIconComponent"; import { @@ -395,6 +395,28 @@ export default function NodeToolbarComponent({ tool_mode: data.node!.tool_mode ?? false, }); + const handleConfirm = useCallback(() => { + addFlow({ + flow: flowComponent, + override: true, + }); + setSuccessData({ title: `${data.id} successfully overridden!` }); + setShowOverrideModal(false); + }, [flowComponent, setSuccessData, setShowOverrideModal]); + + const handleClose = useCallback(() => { + setShowOverrideModal(false); + }, []); + + const handleCancel = useCallback(() => { + addFlow({ + flow: flowComponent, + override: true, + }); + setSuccessData({ title: "New component successfully saved!" }); + setShowOverrideModal(false); + }, [flowComponent, setSuccessData, setShowOverrideModal]); + return ( <>
@@ -754,29 +776,10 @@ export default function NodeToolbarComponent({ { - addFlow({ - flow: flowComponent, - override: true, - }); - setSuccessData({ title: `${data.id} successfully overridden!` }); - setShowOverrideModal(false); - }} - onClose={() => setShowOverrideModal(false)} - onCancel={() => { - addFlow({ - flow: flowComponent, - override: true, - }); - setSuccessData({ title: "New component successfully saved!" }); - setShowOverrideModal(false); - }} + title="Replace" + onConfirm={handleConfirm} + onClose={handleClose} + onCancel={handleCancel} > diff --git a/src/frontend/tests/core/integrations/decisionFlow.spec.ts b/src/frontend/tests/core/integrations/decisionFlow.spec.ts index 52c1be1be..52993eae3 100644 --- a/src/frontend/tests/core/integrations/decisionFlow.spec.ts +++ b/src/frontend/tests/core/integrations/decisionFlow.spec.ts @@ -11,7 +11,7 @@ async function zoomOut(page: Page, times: number = 4) { test( "should create a flow with decision", - { tag: ["@release", "@components"] }, + { tag: ["@release", "@components", "@workflow"] }, async ({ page }) => { test.skip(