Browse Source

perf: optimize edge rendering for large canvases

Replace broad state.nodes subscriptions in EditableEdge and ReferenceEdge
with narrow Zustand selectors that return primitives, eliminating ~50
unnecessary edge re-renders per node interaction. Share gradient defs
across all edges via SharedEdgeGradients (10 gradients vs 50+ per-edge
defs). Replace SVG blur(6px) filter with layered semi-transparent strokes
for loading glow, avoiding expensive filter evaluation on Windows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 4 months ago
parent
commit
ab92cb42c6
  1. 3
      src/components/WorkflowCanvas.tsx
  2. 39
      src/components/__tests__/EditableEdge.test.tsx
  3. 53
      src/components/__tests__/ReferenceEdge.test.tsx
  4. 66
      src/components/edges/EditableEdge.tsx
  5. 34
      src/components/edges/ReferenceEdge.tsx
  6. 52
      src/components/edges/SharedEdgeGradients.tsx
  7. 1
      src/components/edges/index.ts

3
src/components/WorkflowCanvas.tsx

@ -49,7 +49,7 @@ import {
// Lazy-load GLBViewerNode to avoid bundling three.js for users who don't use 3D nodes
const GLBViewerNode = dynamic(() => import("./nodes/GLBViewerNode").then(mod => ({ default: mod.GLBViewerNode })), { ssr: false });
import { EditableEdge, ReferenceEdge } from "./edges";
import { EditableEdge, ReferenceEdge, SharedEdgeGradients } from "./edges";
import { ConnectionDropMenu, MenuAction } from "./ConnectionDropMenu";
import { MultiSelectToolbar } from "./MultiSelectToolbar";
import { EdgeToolbar } from "./EdgeToolbar";
@ -2008,6 +2008,7 @@ export function WorkflowCanvas() {
animated: false,
}}
>
<SharedEdgeGradients />
<GroupBackgroundsPortal />
<GroupControlsOverlay />
<Background color="#404040" gap={20} size={1} />

39
src/components/__tests__/EditableEdge.test.tsx

@ -133,9 +133,10 @@ describe("EditableEdge", () => {
</TestWrapper>
);
// Check that gradient is defined
const gradient = container.querySelector("linearGradient");
expect(gradient).toBeInTheDocument();
// Edges now reference shared gradient IDs instead of per-edge defs
const basePath = container.querySelector(".react-flow__edge-path");
const stroke = basePath?.getAttribute("style") ?? "";
expect(stroke).toContain("edge-grad-image-");
});
it("should use blue color for prompt handle type", () => {
@ -145,8 +146,9 @@ describe("EditableEdge", () => {
</TestWrapper>
);
const gradient = container.querySelector("linearGradient");
expect(gradient).toBeInTheDocument();
const basePath = container.querySelector(".react-flow__edge-path");
const stroke = basePath?.getAttribute("style") ?? "";
expect(stroke).toContain("edge-grad-prompt-");
});
it("should use orange color when edge is paused", () => {
@ -160,8 +162,9 @@ describe("EditableEdge", () => {
</TestWrapper>
);
const gradient = container.querySelector("linearGradient");
expect(gradient).toBeInTheDocument();
const basePath = container.querySelector(".react-flow__edge-path");
const stroke = basePath?.getAttribute("style") ?? "";
expect(stroke).toContain("edge-grad-pause-");
});
});
@ -278,13 +281,10 @@ describe("EditableEdge", () => {
</TestWrapper>
);
// Gradient should exist with active opacity values
const gradient = container.querySelector("linearGradient");
expect(gradient).toBeInTheDocument();
// Check first stop has opacity 1 (active state)
const stops = gradient?.querySelectorAll("stop");
expect(stops?.[0]?.getAttribute("stop-opacity")).toBe("1");
// Should reference the "active" shared gradient
const basePath = container.querySelector(".react-flow__edge-path");
const stroke = basePath?.getAttribute("style") ?? "";
expect(stroke).toContain("-active");
});
it("should have dimmed opacity when not connected to selected node", () => {
@ -300,13 +300,10 @@ describe("EditableEdge", () => {
</TestWrapper>
);
// Gradient should exist with dimmed opacity values
const gradient = container.querySelector("linearGradient");
expect(gradient).toBeInTheDocument();
// Check first stop has lower opacity (dimmed state)
const stops = gradient?.querySelectorAll("stop");
expect(stops?.[0]?.getAttribute("stop-opacity")).toBe("0.25");
// Should reference the "dimmed" shared gradient
const basePath = container.querySelector(".react-flow__edge-path");
const stroke = basePath?.getAttribute("style") ?? "";
expect(stroke).toContain("-dimmed");
});
});

53
src/components/__tests__/ReferenceEdge.test.tsx

@ -115,13 +115,10 @@ describe("ReferenceEdge", () => {
</TestWrapper>
);
// Check that gradient is defined with reference color
const gradient = container.querySelector("linearGradient");
expect(gradient).toBeInTheDocument();
// Gradient ID should contain "reference"
const gradientId = gradient?.getAttribute("id");
expect(gradientId).toContain("reference");
// Edges now reference shared gradient IDs instead of per-edge defs
const basePath = container.querySelector(".react-flow__edge-path");
const stroke = basePath?.getAttribute("style") ?? "";
expect(stroke).toContain("edge-grad-reference-");
});
});
@ -169,12 +166,10 @@ describe("ReferenceEdge", () => {
</TestWrapper>
);
const gradient = container.querySelector("linearGradient");
expect(gradient).toBeInTheDocument();
// Check that gradient ID contains "active"
const gradientId = gradient?.getAttribute("id");
expect(gradientId).toContain("active");
// Should reference the "active" shared gradient
const basePath = container.querySelector(".react-flow__edge-path");
const stroke = basePath?.getAttribute("style") ?? "";
expect(stroke).toContain("-active");
});
it("should have dimmed opacity when not connected to selected node", () => {
@ -190,12 +185,10 @@ describe("ReferenceEdge", () => {
</TestWrapper>
);
const gradient = container.querySelector("linearGradient");
expect(gradient).toBeInTheDocument();
// Check that gradient ID contains "dimmed"
const gradientId = gradient?.getAttribute("id");
expect(gradientId).toContain("dimmed");
// Should reference the "dimmed" shared gradient
const basePath = container.querySelector(".react-flow__edge-path");
const stroke = basePath?.getAttribute("style") ?? "";
expect(stroke).toContain("-dimmed");
});
it("should be dimmed when no nodes are selected", () => {
@ -211,12 +204,10 @@ describe("ReferenceEdge", () => {
</TestWrapper>
);
const gradient = container.querySelector("linearGradient");
expect(gradient).toBeInTheDocument();
// When no nodes selected, edge should be dimmed
const gradientId = gradient?.getAttribute("id");
expect(gradientId).toContain("dimmed");
// Should reference the "dimmed" shared gradient
const basePath = container.querySelector(".react-flow__edge-path");
const stroke = basePath?.getAttribute("style") ?? "";
expect(stroke).toContain("-dimmed");
});
});
@ -249,9 +240,9 @@ describe("ReferenceEdge", () => {
</TestWrapper>
);
const gradient = container.querySelector("linearGradient");
const gradientId = gradient?.getAttribute("id");
expect(gradientId).toContain("active");
const basePath = container.querySelector(".react-flow__edge-path");
const stroke = basePath?.getAttribute("style") ?? "";
expect(stroke).toContain("-active");
});
it("should highlight when target node is selected", () => {
@ -267,9 +258,9 @@ describe("ReferenceEdge", () => {
</TestWrapper>
);
const gradient = container.querySelector("linearGradient");
const gradientId = gradient?.getAttribute("id");
expect(gradientId).toContain("active");
const basePath = container.querySelector(".react-flow__edge-path");
const stroke = basePath?.getAttribute("style") ?? "";
expect(stroke).toContain("-active");
});
});
});

66
src/components/edges/EditableEdge.tsx

@ -10,6 +10,7 @@ import {
} from "@xyflow/react";
import { useWorkflowStore } from "@/store/workflowStore";
import { NanoBananaNodeData, WorkflowEdgeData } from "@/types";
import { getSharedGradientId } from "./SharedEdgeGradients";
interface EdgeData extends WorkflowEdgeData {
offsetX?: number;
@ -43,33 +44,26 @@ export function EditableEdge({
}: EdgeProps) {
const { setEdges } = useReactFlow();
const edgeStyle = useWorkflowStore((state) => state.edgeStyle);
// Subscribe to nodes array to get updates when node data changes
const nodes = useWorkflowStore((state) => state.nodes);
const [isDragging, setIsDragging] = useState(false);
// Check if any node is selected and if this edge is connected to it
const isConnectedToSelection = useMemo(() => {
const selectedNodes = nodes.filter((n) => n.selected);
if (selectedNodes.length === 0) return false; // No selection, show all dimmed
// Check if this edge connects to any selected node
return selectedNodes.some((n) => n.id === source || n.id === target);
}, [nodes, source, target]);
// Narrow selector: returns boolean, only re-renders when selection relevance changes
const isConnectedToSelection = useWorkflowStore((state) => {
const selected = state.nodes.filter((n) => n.selected);
if (selected.length === 0) return false;
return selected.some((n) => n.id === source || n.id === target);
});
const edgeData = data as EdgeData | undefined;
const offsetX = edgeData?.offsetX ?? 0;
const offsetY = edgeData?.offsetY ?? 0;
const hasPause = edgeData?.hasPause ?? false;
// Check if target node is a Generate node that's currently loading
const isTargetLoading = useMemo(() => {
const targetNode = nodes.find((n) => n.id === target);
if (targetNode?.type === "nanoBanana") {
const nodeData = targetNode.data as NanoBananaNodeData;
return nodeData.status === "loading";
}
return false;
}, [target, nodes]);
// Narrow selector: only re-renders when target loading status changes
const isTargetLoading = useWorkflowStore((state) => {
const targetNode = state.nodes.find((n) => n.id === target);
if (targetNode?.type !== "nanoBanana") return false;
return (targetNode.data as NanoBananaNodeData).status === "loading";
});
// Determine edge color based on handle type (orange if paused)
const edgeColor = useMemo(() => {
@ -81,12 +75,12 @@ export function EditableEdge({
return EDGE_COLORS.default;
}, [hasPause, sourceHandleId, targetHandleId]);
// Generate a unique gradient ID based on edge color and selection state
// Reference shared gradient by color key + selection state
const gradientId = useMemo(() => {
const colorKey = hasPause ? "pause" : (sourceHandleId || targetHandleId || "default");
const selectionKey = isConnectedToSelection ? "active" : "dimmed";
return `edge-gradient-${colorKey}-${selectionKey}-${id}`;
}, [hasPause, sourceHandleId, targetHandleId, isConnectedToSelection, id]);
return getSharedGradientId(colorKey, selectionKey);
}, [hasPause, sourceHandleId, targetHandleId, isConnectedToSelection]);
// Calculate the path based on edge style
const [edgePath, labelX, labelY] = useMemo(() => {
@ -181,15 +175,6 @@ export function EditableEdge({
return (
<>
{/* SVG gradient definition for bright-dim-bright effect */}
<defs>
<linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor={edgeColor} stopOpacity={isConnectedToSelection ? 1 : 0.25} />
<stop offset="50%" stopColor={edgeColor} stopOpacity={isConnectedToSelection ? 0.55 : 0.1} />
<stop offset="100%" stopColor={edgeColor} stopOpacity={isConnectedToSelection ? 1 : 0.25} />
</linearGradient>
</defs>
<BaseEdge
id={id}
path={edgePath}
@ -206,18 +191,25 @@ export function EditableEdge({
{/* Animated pulse overlay when target is loading */}
{isTargetLoading && (
<>
{/* Glow effect behind the pulse */}
{/* Outer glow — replaces blur(6px) filter for better perf on Windows */}
<path
d={edgePath}
fill="none"
stroke={`url(#${gradientId})`}
strokeWidth={10}
strokeWidth={20}
strokeLinecap="round"
strokeLinejoin="round"
style={{
opacity: 0.2,
filter: "blur(6px)",
}}
opacity={0.06}
/>
{/* Inner glow */}
<path
d={edgePath}
fill="none"
stroke={`url(#${gradientId})`}
strokeWidth={12}
strokeLinecap="round"
strokeLinejoin="round"
opacity={0.12}
/>
{/* Animated flowing pulse using stroke-dasharray */}
<path

34
src/components/edges/ReferenceEdge.tsx

@ -7,9 +7,7 @@ import {
getBezierPath,
} from "@xyflow/react";
import { useWorkflowStore } from "@/store/workflowStore";
// Grey color for reference connections (dimmed for softer appearance)
const REFERENCE_COLOR = "#52525b";
import { getSharedGradientId } from "./SharedEdgeGradients";
export function ReferenceEdge({
id,
@ -25,16 +23,13 @@ export function ReferenceEdge({
target,
}: EdgeProps) {
const edgeStyle = useWorkflowStore((state) => state.edgeStyle);
const nodes = useWorkflowStore((state) => state.nodes);
// Check if any node is selected and if this edge is connected to it
const isConnectedToSelection = useMemo(() => {
const selectedNodes = nodes.filter((n) => n.selected);
if (selectedNodes.length === 0) return false; // No selection, show all dimmed
// Check if this edge connects to any selected node
return selectedNodes.some((n) => n.id === source || n.id === target);
}, [nodes, source, target]);
// Narrow selector: returns boolean, only re-renders when selection relevance changes
const isConnectedToSelection = useWorkflowStore((state) => {
const selected = state.nodes.filter((n) => n.selected);
if (selected.length === 0) return false;
return selected.some((n) => n.id === source || n.id === target);
});
// Calculate the path - always use curved for reference edges for softer look
const [edgePath] = useMemo(() => {
@ -49,23 +44,14 @@ export function ReferenceEdge({
});
}, [sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition]);
// Generate a unique gradient ID based on selection state
// Reference shared gradient by selection state
const gradientId = useMemo(() => {
const selectionKey = isConnectedToSelection ? "active" : "dimmed";
return `reference-gradient-${selectionKey}-${id}`;
}, [isConnectedToSelection, id]);
return getSharedGradientId("reference", selectionKey);
}, [isConnectedToSelection]);
return (
<>
{/* SVG gradient definition for bright-dim-bright effect */}
<defs>
<linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor={REFERENCE_COLOR} stopOpacity={isConnectedToSelection ? 1 : 0.25} />
<stop offset="50%" stopColor={REFERENCE_COLOR} stopOpacity={isConnectedToSelection ? 0.55 : 0.1} />
<stop offset="100%" stopColor={REFERENCE_COLOR} stopOpacity={isConnectedToSelection ? 1 : 0.25} />
</linearGradient>
</defs>
<BaseEdge
id={id}
path={edgePath}

52
src/components/edges/SharedEdgeGradients.tsx

@ -0,0 +1,52 @@
"use client";
// Shared SVG gradient definitions for all edge types.
// Rendered once inside the React Flow SVG layer to avoid duplicating
// <defs>/<linearGradient> in every edge component.
const EDGE_COLORS: Record<string, string> = {
image: "#0d9668",
prompt: "#2563eb",
default: "#64748b",
pause: "#ea580c",
reference: "#52525b",
};
const SELECTION_STATES = ["active", "dimmed"] as const;
function gradientStops(color: string, active: boolean) {
return (
<>
<stop offset="0%" stopColor={color} stopOpacity={active ? 1 : 0.25} />
<stop offset="50%" stopColor={color} stopOpacity={active ? 0.55 : 0.1} />
<stop offset="100%" stopColor={color} stopOpacity={active ? 1 : 0.25} />
</>
);
}
export function getSharedGradientId(colorKey: string, selectionKey: "active" | "dimmed") {
return `edge-grad-${colorKey}-${selectionKey}`;
}
export function SharedEdgeGradients() {
return (
<svg style={{ position: "absolute", width: 0, height: 0 }}>
<defs>
{Object.entries(EDGE_COLORS).flatMap(([colorKey, color]) =>
SELECTION_STATES.map((sel) => (
<linearGradient
key={`${colorKey}-${sel}`}
id={getSharedGradientId(colorKey, sel)}
x1="0%"
y1="0%"
x2="100%"
y2="0%"
>
{gradientStops(color, sel === "active")}
</linearGradient>
))
)}
</defs>
</svg>
);
}

1
src/components/edges/index.ts

@ -1,2 +1,3 @@
export { EditableEdge } from "./EditableEdge";
export { ReferenceEdge } from "./ReferenceEdge";
export { SharedEdgeGradients } from "./SharedEdgeGradients";

Loading…
Cancel
Save