-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Explore WebGL for indicator and overlay rendering #7437
Description
Problem
The current indicator and overlay rendering system has significant performance issues:
-
Complete remount on page changes - When switching pages, all indicator DOM nodes are destroyed and recreated because React keys are
id + '_indicator'and shape IDs differ between pages. For pages with 100+ shapes, this causes noticeable lag. -
All indicators render when any changes - When the hovered shape changes, or selection changes, the entire
renderingShapesarray is mapped and allShapeIndicatorcomponents render (even though most are memoized, they still go through memo comparison). -
DOM overhead - Each indicator requires its own SVG element with nested groups, meaning hundreds of DOM nodes for a moderately complex canvas. The overlays layer contains many SVG elements even when most indicators are hidden via
display: none. -
Multiple memo layers - The current implementation uses three layers of memo (
DefaultShapeIndicator→InnerIndicator→EvenInnererIndicator) to allow hooks at each level, but this adds prop drilling and comparison overhead.
Current architecture
Component hierarchy
.tl-overlays (div, contain: strict)
└── .tl-html-layer (div, scaled/translated)
└── DefaultShapeIndicators (memo)
└── [for each rendering shape]
└── DefaultShapeIndicator (memo)
└── svg.tl-overlays__item
└── g.tl-shape-indicator
└── InnerIndicator (memo)
└── EvenInnererIndicator (memo, custom comparison)
└── ShapeUtil.indicator() → SVG path/rect
Key files
packages/editor/src/lib/components/default-components/DefaultShapeIndicators.tsxpackages/editor/src/lib/components/default-components/DefaultShapeIndicator.tsxpackages/editor/src/lib/components/default-components/DefaultCanvas.tsx
Current optimizations
- CSS
contain: stricton.tl-overlaysprevents layout thrashing - Transform updates via direct DOM manipulation (
useQuickReactor) bypass React - Custom memo comparison only checks
shape.propsandshape.meta - Visibility culling filters off-screen shapes via
notVisibleShapesderivation (recently optimized in perf: optimize notVisibleShapes derivation with inlined bounds checks #7429) idsToDisplaydiffing prevents unnecessary re-renders when selection hasn't changed
Proposed alternatives
Option 1: WebGL overlay canvas
Replace the entire indicator/overlay layer with a WebGL canvas that renders indicators directly to GPU.
Pros:
- Single canvas element instead of hundreds of DOM nodes
- GPU-accelerated rendering - batch all indicators in one draw call
- Near-instant "page switch" - just clear and redraw geometry
- Already have
WebGLManagerpattern intemplates/shader/ - Smooth zoom/pan - matrix transforms on GPU
Cons:
- Need to reimplement indicator geometry rendering in WebGL
- Lose React component model for indicators
- More complex debugging
- Custom shapes need WebGL indicator implementation
- Need fallback for WebGL unavailability
Approach:
- Create
IndicatorWebGLManagerextending existingWebGLManagerpattern - Collect all indicator geometry (paths, rects, etc.) each frame
- Batch render as stroked paths using a line shader
- Use instanced rendering for common shapes
Option 2: Canvas 2D overlay
Use HTML5 Canvas 2D context instead of WebGL.
Pros:
- Simpler than WebGL - no shaders needed
- Good for moderate shape counts
- Easy to render SVG paths via
Path2D
Cons:
- CPU-bound, less scalable than WebGL
- Still need to reimplement indicator rendering
- Performance varies significantly by browser/OS
Option 3: Single SVG with reused elements
Keep SVG but consolidate all indicators into a single <svg> with reusable <use> elements.
Pros:
- Smaller change from current approach
- Native SVG features (styling, accessibility)
- Browser handles rendering optimization
Cons:
- Still DOM-based with associated overhead
<use>has browser-specific quirks- May not solve fundamental remounting issue
Option 4: Hybrid approach
Use WebGL for common indicators (rectangles, simple paths) and fall back to SVG for complex custom indicators.
Pros:
- Best of both worlds
- Graceful degradation for custom shapes
- Incremental migration path
Cons:
- More complex architecture
- Two rendering paths to maintain
Option 5: Virtual list / windowing
Only mount indicator components for shapes near viewport center, lazy-mount others.
Pros:
- Minimal change to existing architecture
- Scales with viewport, not total shapes
- Works with existing custom indicators
Cons:
- Edge cases around viewport boundaries
- Doesn't solve hover/selection re-render issue
- Still many DOM nodes for dense canvases
Performance benchmarks needed
To properly evaluate these options, we should benchmark:
- Page switch latency with varying shape counts (50, 100, 500, 1000 shapes)
- Hover indicator change latency
- Selection change latency
- Memory usage for indicator DOM nodes
- Frame time during pan/zoom with all indicators visible
Recommendation
I'd recommend exploring Option 1 (WebGL) as the primary investigation, with Option 4 (Hybrid) as a fallback strategy. The existing WebGLManager in templates/shader/ provides a solid foundation for integration with tldraw's reactive system.
Key questions to answer:
- Can we efficiently batch heterogeneous indicator geometries?
- What's the fallback story for custom ShapeUtil indicators?
- How do we handle indicator interactions (if any)?
- Performance target: <16ms for page switch regardless of shape count
References
- WebGL vs Canvas performance comparisons
- PixiJS - WebGL 2D renderer (for reference)
- Existing
templates/shader/WebGLManager pattern - Recent optimization: perf: optimize notVisibleShapes derivation with inlined bounds checks #7429 (notVisibleShapes derivation)