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 // 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 }); 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 { ConnectionDropMenu, MenuAction } from "./ConnectionDropMenu";
import { MultiSelectToolbar } from "./MultiSelectToolbar"; import { MultiSelectToolbar } from "./MultiSelectToolbar";
import { EdgeToolbar } from "./EdgeToolbar"; import { EdgeToolbar } from "./EdgeToolbar";
@ -2008,6 +2008,7 @@ export function WorkflowCanvas() {
animated: false, animated: false,
}} }}
> >
<SharedEdgeGradients />
<GroupBackgroundsPortal /> <GroupBackgroundsPortal />
<GroupControlsOverlay /> <GroupControlsOverlay />
<Background color="#404040" gap={20} size={1} /> <Background color="#404040" gap={20} size={1} />

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

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

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

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

66
src/components/edges/EditableEdge.tsx

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

34
src/components/edges/ReferenceEdge.tsx

@ -7,9 +7,7 @@ import {
getBezierPath, getBezierPath,
} from "@xyflow/react"; } from "@xyflow/react";
import { useWorkflowStore } from "@/store/workflowStore"; import { useWorkflowStore } from "@/store/workflowStore";
import { getSharedGradientId } from "./SharedEdgeGradients";
// Grey color for reference connections (dimmed for softer appearance)
const REFERENCE_COLOR = "#52525b";
export function ReferenceEdge({ export function ReferenceEdge({
id, id,
@ -25,16 +23,13 @@ export function ReferenceEdge({
target, target,
}: EdgeProps) { }: EdgeProps) {
const edgeStyle = useWorkflowStore((state) => state.edgeStyle); 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 // Narrow selector: returns boolean, only re-renders when selection relevance changes
return selectedNodes.some((n) => n.id === source || n.id === target); const isConnectedToSelection = useWorkflowStore((state) => {
}, [nodes, source, target]); 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 // Calculate the path - always use curved for reference edges for softer look
const [edgePath] = useMemo(() => { const [edgePath] = useMemo(() => {
@ -49,23 +44,14 @@ export function ReferenceEdge({
}); });
}, [sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition]); }, [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 gradientId = useMemo(() => {
const selectionKey = isConnectedToSelection ? "active" : "dimmed"; const selectionKey = isConnectedToSelection ? "active" : "dimmed";
return `reference-gradient-${selectionKey}-${id}`; return getSharedGradientId("reference", selectionKey);
}, [isConnectedToSelection, id]); }, [isConnectedToSelection]);
return ( 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 <BaseEdge
id={id} id={id}
path={edgePath} 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 { EditableEdge } from "./EditableEdge";
export { ReferenceEdge } from "./ReferenceEdge"; export { ReferenceEdge } from "./ReferenceEdge";
export { SharedEdgeGradients } from "./SharedEdgeGradients";

Loading…
Cancel
Save