-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Centralize shape culling display updates to reduce per-shape subscriptions #7831
Description
Problem
Each Shape component in packages/editor/src/lib/components/Shape.tsx has its own useQuickReactor that subscribes to editor.getCulledShapes() to update its display property:
useQuickReactor(
'set display',
() => {
const shape = editor.getShape(id)
if (!shape) return
const culledShapes = editor.getCulledShapes()
const isCulled = culledShapes.has(id)
if (isCulled !== memoizedStuffRef.current.isCulled) {
setStyleProperty(containerRef.current, 'display', isCulled ? 'none' : 'block')
setStyleProperty(bgContainerRef.current, 'display', isCulled ? 'none' : 'block')
memoizedStuffRef.current.isCulled = isCulled
}
},
[editor]
)With N shapes on the canvas, this creates N separate subscriptions all watching the same computed value. When culled shapes change, all N reactors run even though most shapes' culling state hasn't changed.
Proposed solution
Create a centralized registry of shape containers that allows a single reactor to imperatively update display properties:
- Registry: A
Map<TLShapeId, { container, bgContainer }>that tracks shape container refs - Registration: Shape components register their refs on mount, unregister on unmount
- Single reactor: One centralized subscription that diffs the culled shapes set and only updates containers whose state changed
This changes the complexity from O(N) subscriptions to O(1) subscription, with O(changed shapes) DOM updates.
Implementation sketch
// Registry (could live on Editor or as a separate manager)
class ShapeContainerRegistry {
private containers = new Map<TLShapeId, {
container: HTMLDivElement | null
bgContainer: HTMLDivElement | null
}>()
register(id, container, bgContainer) { ... }
unregister(id) { ... }
updateCulling(culledShapes: Set<TLShapeId>, prevCulled: Set<TLShapeId>) {
// Only update shapes whose culling state changed
for (const id of culledShapes) {
if (!prevCulled.has(id)) {
// Newly culled - hide
}
}
for (const id of prevCulled) {
if (!culledShapes.has(id)) {
// Newly visible - show
}
}
}
}
// Single reactor in DefaultCanvas/ShapesToDisplay
function CullingController() {
const editor = useEditor()
const prevCulledRef = useRef<Set<TLShapeId>>(new Set())
useQuickReactor('update culling', () => {
const culledShapes = editor.getCulledShapes()
editor.shapeContainerRegistry.updateCulling(culledShapes, prevCulledRef.current)
prevCulledRef.current = culledShapes
}, [editor])
return null
}Prior art
There's already a similar centralized pattern in DefaultCanvas.tsx with ReflowIfNeeded (lines 410-429) that has a single reactor watching getCulledShapes() for a different purpose.
Considerations
- Registry could live on the Editor class or be provided via React context
- Need to ensure registration happens before the centralized reactor first runs
- Initial culling state needs to be set correctly on mount