Repository navigation
Incremental updates to the SVG renderer #47
Description
Context
The SVG renderer repaints everything on every change. In packages/svg/src/renderer.ts, paint() calls clearChildren(objectsG) and then drawEntity for every entity:
- on every
entities:changed, even when only one entity was updated; - on every
selection:change, even when only two entities changed state.
During a drag, core.update runs on every pointer move, so the whole scene is rebuilt about 60 times per second. That is fine for small scenes, but it gets slow with hundreds or thousands of entities (a stadium seat map, a large warehouse).
What changes
Keep one <g> per entity and update only what changed. The entities:changed event already carries what's needed: { type: 'add' | 'update' | 'remove', ids }.
- Keep a
Map<EntityId, SVGGElement>in the renderer. add→ create the entity's<g>, calldrawEntityinto it, and insert it in its layer group at the right position (insertion order = z-order).update→ clear only that entity's<g>and calldrawEntityagain. If the layer changed, move the<g>to the new layer group.remove→ remove the<g>and the map entry.selection:change→ redraw only the entities whose selected state changed (diff the previous and current selection).rerender()keeps doing a full repaint, as an escape hatch.
Selection overlay and resize handles can stay as they are in a first PR; they only draw selected entities.
Affected files
| File | Change |
|---|---|
packages/svg/src/renderer.ts |
Incremental painting by event type |
packages/svg/src/__tests__/ |
Tests for each event type and for z-order |
The React adapter already renders with keyed elements (<g key={entity.id}>), so React's reconciliation covers part of this. Profiling it is a possible follow-up issue, not part of this one.
Invariants to preserve
- Rendered output (DOM order, layer groups,
data-spatial-layerattributes) is identical to a full repaint after any sequence of events. - Z-order: layer order first, then insertion order inside each layer.
drawEntitykeeps the same signature and receives the same{ g, selected }context.rerender()anddestroy()keep working.
Implementation checklist
- Entity →
<g>map and incrementaladd/update/remove - Selection diff: redraw only entities whose state changed
- Tests: each event type, a layer change, z-order after add/remove, and "incremental output equals full repaint"
- A spy test showing
drawEntityis called only for the changed entity onupdate - Optional: a quick benchmark note in the PR (e.g. 2,000 entities, drag one)
- Changeset (
patchorminorfor svg) - Run
pnpm typecheck && pnpm test && pnpm lint
Activity