platform(fix): Improve performance of builder (#9435)

1. Remove isHovered / onMouseEnter / onMouseLeave state updates
2. Wrap Custom Node in React.memo
3. Avoid re-renders for context menus
This commit is contained in:
Swifty
2025-02-07 10:38:50 +01:00
committed by GitHub
parent 797916cf14
commit c693875951
3 changed files with 801 additions and 774 deletions

View File

@@ -40,7 +40,6 @@ export function CustomEdge({
targetY,
markerEnd,
}: EdgeProps<CustomEdge>) {
const [isHovered, setIsHovered] = useState(false);
const [beads, setBeads] = useState<{
beads: Bead[];
created: number;
@@ -182,13 +181,7 @@ export function CustomEdge({
<BaseEdge
path={svgPath}
markerEnd={markerEnd}
style={{
strokeWidth: (isHovered ? 3 : 2) + (data?.isStatic ? 0.5 : 0),
stroke:
(data?.edgeColor ?? "#555555") +
(selected || isHovered ? "" : "80"),
strokeDasharray: data?.isStatic ? "5 3" : "0",
}}
className={`transition-all duration-200 ${data?.isStatic ? "[stroke-dasharray:5_3]" : "[stroke-dasharray:0]"} [stroke-width:${data?.isStatic ? 2.5 : 2}px] hover:[stroke-width:${data?.isStatic ? 3.5 : 3}px] ${selected ? `[stroke:${data?.edgeColor ?? "#555555"}]` : `[stroke:${data?.edgeColor ?? "#555555"}80] hover:[stroke:${data?.edgeColor ?? "#555555"}]`}`}
/>
<path
d={svgPath}
@@ -196,8 +189,6 @@ export function CustomEdge({
strokeOpacity={0}
strokeWidth={20}
className="react-flow__edge-interaction"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
/>
<EdgeLabelRenderer>
<div
@@ -209,9 +200,7 @@ export function CustomEdge({
className="edge-label-renderer"
>
<button
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={`edge-label-button ${isHovered ? "visible" : ""}`}
className="edge-label-button opacity-0 transition-opacity duration-200 hover:opacity-100"
onClick={onEdgeRemoveClick}
>
<X className="size-4" />

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import { BlockIOSubSchema } from "@/lib/autogpt-server-api/types";
import { beautifyString, getTypeBgColor, getTypeTextColor } from "@/lib/utils";
import { FC } from "react";
import { FC, memo, useCallback } from "react";
import { Handle, Position } from "@xyflow/react";
import SchemaTooltip from "./SchemaTooltip";
@@ -13,6 +13,32 @@ type HandleProps = {
title?: string;
};
// Move the constant out of the component to avoid re-creation on every render.
const TYPE_NAME: Record<string, string> = {
string: "text",
number: "number",
integer: "integer",
boolean: "true/false",
object: "object",
array: "list",
null: "null",
};
// Extract and memoize the Dot component so that it doesn't re-render unnecessarily.
const Dot: FC<{ isConnected: boolean; type?: string }> = memo(
({ isConnected, type }) => {
const color = isConnected
? getTypeBgColor(type || "any")
: "border-gray-300 dark:border-gray-600";
return (
<div
className={`${color} m-1 h-4 w-4 rounded-full border-2 bg-white transition-colors duration-100 group-hover:bg-gray-300 dark:bg-slate-800 dark:group-hover:bg-gray-700`}
/>
);
},
);
Dot.displayName = "Dot";
const NodeHandle: FC<HandleProps> = ({
keyName,
schema,
@@ -21,17 +47,9 @@ const NodeHandle: FC<HandleProps> = ({
side,
title,
}) => {
const typeName: Record<string, string> = {
string: "text",
number: "number",
integer: "integer",
boolean: "true/false",
object: "object",
array: "list",
null: "null",
};
const typeClass = `text-sm ${getTypeTextColor(schema.type || "any")} ${side === "left" ? "text-left" : "text-right"}`;
const typeClass = `text-sm ${getTypeTextColor(schema.type || "any")} ${
side === "left" ? "text-left" : "text-right"
}`;
const label = (
<div className="flex flex-grow flex-row">
@@ -40,25 +58,27 @@ const NodeHandle: FC<HandleProps> = ({
{isRequired ? "*" : ""}
</span>
<span className={`${typeClass} flex items-end`}>
({typeName[schema.type as keyof typeof typeName] || "any"})
({TYPE_NAME[schema.type as keyof typeof TYPE_NAME] || "any"})
</span>
</div>
);
const Dot = () => {
const color = isConnected
? getTypeBgColor(schema.type || "any")
: "border-gray-300 dark:border-gray-600";
return (
<div
className={`${color} m-1 h-4 w-4 rounded-full border-2 bg-white transition-colors duration-100 group-hover:bg-gray-300 dark:bg-slate-800 dark:group-hover:bg-gray-700`}
/>
);
};
// Use a native HTML onContextMenu handler instead of wrapping a large node with a Radix ContextMenu trigger.
const handleContextMenu = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
e.preventDefault();
// Optionally, you can trigger a custom, lightweight context menu here.
},
[],
);
if (side === "left") {
return (
<div key={keyName} className="handle-container">
<div
key={keyName}
className="handle-container"
onContextMenu={handleContextMenu}
>
<Handle
type="target"
data-testid={`input-handle-${keyName}`}
@@ -67,7 +87,7 @@ const NodeHandle: FC<HandleProps> = ({
className="group -ml-[38px]"
>
<div className="pointer-events-none flex items-center">
<Dot />
<Dot isConnected={isConnected} type={schema.type} />
{label}
</div>
</Handle>
@@ -76,7 +96,11 @@ const NodeHandle: FC<HandleProps> = ({
);
} else {
return (
<div key={keyName} className="handle-container justify-end">
<div
key={keyName}
className="handle-container justify-end"
onContextMenu={handleContextMenu}
>
<Handle
type="source"
data-testid={`output-handle-${keyName}`}
@@ -86,7 +110,7 @@ const NodeHandle: FC<HandleProps> = ({
>
<div className="pointer-events-none flex items-center">
{label}
<Dot />
<Dot isConnected={isConnected} type={schema.type} />
</div>
</Handle>
</div>
@@ -94,4 +118,4 @@ const NodeHandle: FC<HandleProps> = ({
}
};
export default NodeHandle;
export default memo(NodeHandle);