Skip to content

Commit a5d836e

Browse files
perf: memoize canvas hot-path selection and connector lookups
1 parent 9cffd35 commit a5d836e

1 file changed

Lines changed: 40 additions & 29 deletions

File tree

components/editor/canvas.tsx

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import React from "react"
44
import dynamic from "next/dynamic"
5-
import { useState, useRef, useEffect, useCallback } from "react"
5+
import { useState, useRef, useEffect, useCallback, useMemo } from "react"
66
import { Toolbar } from "./toolbar"
77
import { PropertiesPanel } from "./properties-panel"
88
import { ThemeToggle } from "@/components/theme-toggle"
@@ -166,6 +166,41 @@ export function Canvas({ previewElements }: { previewElements?: PreviewState | n
166166
elementsRef.current = elements
167167
selectedConnectionIdRef.current = selectedConnectionId
168168

169+
const selectedElementIds = useMemo(() => new Set(appState.selection), [appState.selection])
170+
const selectedElements = useMemo(
171+
() => elements.filter((el) => selectedElementIds.has(el.id)),
172+
[elements, selectedElementIds],
173+
)
174+
const elementById = useMemo(() => {
175+
const map = new Map<string, CanvasElement>()
176+
for (const el of elements) {
177+
map.set(el.id, el)
178+
}
179+
return map
180+
}, [elements])
181+
const connectorPreviewAnchor = useMemo(() => {
182+
if (!connectorSource) return null
183+
184+
const element = elementById.get(connectorSource.elementId)
185+
if (!element) return null
186+
187+
const centerX = element.x + element.width / 2
188+
const centerY = element.y + element.height / 2
189+
190+
switch (connectorSource.handle) {
191+
case "left":
192+
return { x: element.x, y: centerY }
193+
case "right":
194+
return { x: element.x + element.width, y: centerY }
195+
case "top":
196+
return { x: centerX, y: element.y }
197+
case "bottom":
198+
return { x: centerX, y: element.y + element.height }
199+
default:
200+
return { x: centerX, y: centerY }
201+
}
202+
}, [connectorSource, elementById])
203+
169204
const centerViewportOnElements = useCallback(() => {
170205
if (elements.length === 0) return
171206

@@ -2042,15 +2077,15 @@ export function Canvas({ previewElements }: { previewElements?: PreviewState | n
20422077
{/* Fixed properties panel */}
20432078
<PropertiesPanel
20442079
appState={appState}
2045-
selectedElements={elements.filter((el) => appState.selection.includes(el.id))}
2080+
selectedElements={selectedElements}
20462081
selectedConnectionId={selectedConnectionId} // Pass selected connection ID
20472082
connections={connections} // Pass connections
20482083
onChange={(updates) => handleAppStateChange(updates)}
20492084
onAction={handleAction}
20502085
/>
20512086

20522087
<ActionsMenu
2053-
selectedElements={elements.filter((el) => appState.selection.includes(el.id))}
2088+
selectedElements={selectedElements}
20542089
onAction={handleAction}
20552090
/>
20562091

@@ -2110,32 +2145,8 @@ export function Canvas({ previewElements }: { previewElements?: PreviewState | n
21102145
{
21112146
connectorSource && connectorPreview && (
21122147
<line
2113-
x1={(() => {
2114-
const el = elements.find((e) => e.id === connectorSource.elementId)
2115-
if (!el) return 0
2116-
const cx = el.x + el.width / 2
2117-
switch (connectorSource.handle) {
2118-
case "left":
2119-
return el.x
2120-
case "right":
2121-
return el.x + el.width
2122-
default:
2123-
return cx
2124-
}
2125-
})()}
2126-
y1={(() => {
2127-
const el = elements.find((e) => e.id === connectorSource.elementId)
2128-
if (!el) return 0
2129-
const cy = el.y + el.height / 2
2130-
switch (connectorSource.handle) {
2131-
case "top":
2132-
return el.y
2133-
case "bottom":
2134-
return el.y + el.height
2135-
default:
2136-
return cy
2137-
}
2138-
})()}
2148+
x1={connectorPreviewAnchor?.x ?? 0}
2149+
y1={connectorPreviewAnchor?.y ?? 0}
21392150
x2={connectorPreview.x}
21402151
y2={connectorPreview.y}
21412152
stroke={getSolidStrokeColor(appState.currentItemStrokeColor)}

0 commit comments

Comments
 (0)