Comfy-Org / Comfy-Org/ComfyUI_frontend
[Draft] Long-term Rendering Architecture Roadmap: Hybrid Canvas+Vue Strategy
- Dominant language
- TypeScript
- Stars
- 2k
- Forks
- 699
- Avg merge
- 1d 7h
- Merged PRs (30d)
- 490
Description
## Background & Motivation
This roadmap document consolidates architectural observations from internal discussions and recent subgraph correctness work. The core insight: our hybrid canvas+Vue rendering architecture has a **structural performance advantage** over React-based node editors (e.g., ReactFlow/xyflow), but we need to systematically address correctness issues and refine the boundary between canvas and Vue worlds.
### Why this matters now
React-based node editors route node position updates through React hooks → prop subscriptions → virtual DOM diff/reconciliation on every mouse-move pixel. This creates a permanent performance ceiling that cannot be optimized away without abandoning the architecture. Known issues in ReactFlow:
- [xyflow #4983](https://github.com/xyflow/xyflow/issues/4983): `React.memo`-wrapped nodes still re-render when other nodes move
- [xyflow #2119](https://github.com/xyflow/xyflow/issues/2119): Major lag when dragging nodes with React 18
- [xyflow #4391](https://github.com/xyflow/xyflow/issues/4391): Node dragging freezes when too fast at scale
Our problems are **correctness issues** (widget resolution in subgraphs, state synchronization), which are tractable and don't permanently cap performance. This document outlines the path to resolving them while preserving our architectural advantage.
---
## Current Architecture
| Layer | Technology | Role |
|---|---|---|
| Node content | Vue 3 DOM components | Widgets, forms, interaction |
| Node layout | Yjs CRDT → `customRef` → Vue reactivity | Reactive position/size |
| Connections | Canvas2D (`pathRenderer.ts`) | Bezier/straight/linear links |
| Pan/Zoom | Single CSS `transform` container | O(1) transform updates |
| Spatial queries | QuadTree | O(log n) hit-testing |
| State authority | `layoutStore.ts` (Yjs-backed) | CRDT-ready operations |
Key design decisions already in place:
- **TransformPane** uses direct DOM manipulation via RAF (not Vue template binding) for pan/zoom
- **Node drag** is RAF-throttled with batch updates
- **Source tracking** (`LayoutSource.Canvas | Vue | DOM | External`) prevents feedback loops
- **Operations** carry `actor`, `timestamp`, `source` metadata — CRDT conflict resolution ready
---
## Roadmap
### Phase 1: Eliminate Dual State (LiteGraph Decoupling)
**Problem:** Node positions exist in two places — Layout Store (Yjs, source of truth) and LiteGraph (`lnode.pos[]`). One-way sync (Layout Store → LiteGraph) works but adds complexity and creates edge cases where LiteGraph mutations bypass the store.
**Goal:** Layout Store becomes the sole authority. LiteGraph is reduced to a connection rendering backend or replaced entirely.
**Steps:**
1. Audit all codepaths where LiteGraph reads/writes node positions directly
2. Redirect remaining LiteGraph position reads to Layout Store
3. Extract connection rendering into a standalone module decoupled from LiteGraph's node model
4. Remove the sync layer (`useLayoutSync.ts`) once LiteGraph no longer owns position state
### Phase 2: Hot-Path Optimization — Bypass Vue Reactivity During Drag
**Problem:** Current drag flow passes through Vue's reactivity system every frame:
```
Pointer event → screenToCanvas → layoutStore.applyOperation
→ Yjs update → customRef trigger → Vue re-render → DOM update
```
RAF throttling helps, but each frame still pays the cost of Yjs serialization, `customRef` trigger propagation, and Vue's reactive scheduling.
**Goal:** During continuous interactions (drag, resize), update the DOM directly. Commit to reactive state only on interaction end.
**Approach:**
```
During drag: pointer event → direct style.transform manipulation (zero framework cost)
On drag end: commit final position to layoutStore (single reactive update)
```
This mirrors the game engine pattern: physics runs every frame, state persistence happens at checkpoints.
### Phase 3: Systematize Subgraph Widget Resolution
**Problem:** Recent fixes show promoted widget resolution is being patched case-by-case. The bugs share a common root: no unified resolution engine that handles arbitrary subgraph nesting depth with clear invariants.
**Recent correctness fixes:**
- #9896 — Widget identity breaks through configure/hydration cycles
- #9885 — Stale `slotMetadata` retained after link disconnect
- #9542 — Non-widget inputs appear as button widgets on nested subgraphs
- #9282 — Nested subgraph promoted widgets don't resolve through multiple levels
- #9012 — Combo widget options empty for PromotedWidgetView
- #9865 — Stale progress bar on SubgraphNode after navigation
- #9510 — Subgraph node ID deduplication hardening
- #9266 — Subgraph output slot labels not updating in v2 renderer
- #9120 — Duplicate links in subgraph unpacking
**Goal:** A recursive resolution engine with well-defined invariants:
1. Every promoted widget resolves to a concrete source at any nesting depth
2. Widget identity is stable across configure/hydration cycles
3. Link state changes (connect/disconnect) always propagate to widget metadata
4. Resolution is tested via property-based tests on random subgraph topologies
### Phase 4: Leverage CRDT Infrastructure
The Yjs foundation is already in place. Progressive rollout:
| Stage | Capability |
|---|---|
| Current | Yjs as local reactive state store |
| Short-term | Undo/Redo via Yjs history (`Y.UndoManager`) |
| Mid-term | Multi-tab/window synchronization (same machine) |
| Long-term | Network-based real-time collaboration (`y-websocket`) |
Operations already carry `actor` and `timestamp` — conflict resolution semantics are architecturally prepared.
### Phase 5: Connection Rendering Evolution (When Needed)
Current Canvas2D is performant for typical workloads. Evolution path if/when bottlenecks are observed:
1. **Now:** Canvas2D with viewport culling (sufficient)
2. **If needed:** WebGL/WebGPU batch rendering for 1000+ connections
3. **Design constraint:** Never move connections to DOM/SVG (avoids ReactFlow's architectural mistake)
Apply YAGNI — only proceed when profiling data justifies it.
---
## Strategic Principles
### Do
- Maintain the hybrid canvas+Vue architecture
- Keep high-frequency spatial operations (drag, pan, zoom) off the framework hot path
- Fix subgraph correctness issues systematically with invariant-based design
- Build incrementally on the CRDT foundation
### Don't
- Move connections to DOM/SVG rendering
- Make node positions depend solely on Vue reactivity during continuous interactions
- Attempt a full LiteGraph removal in one step
- Over-engineer for collaboration before the local experience is solid
---
## Related Work
### Recent Subgraph Correctness PRs
- #9896 — Stabilize subgraph promoted widget identity and rendering
- #9885 — Clear stale widget slotMetadata on link disconnect
- #9542 — Fix non-widget inputs appearing as button widgets on nested subgraphs
- #9282 — Nested subgraph promoted widget resolution
- #9865 — Clear stale progress bar on SubgraphNode after navigation
- #9510 — Extract and harden subgraph node ID deduplication
- #9266 — Fix subgraph output slot labels not updating in v2 renderer
- #9120 — Detect and remove duplicate links in subgraph unpacking
- #9292 — Add tests for nested promoted widget resolution paths
### Backports (indicating stability impact)
- #9616 — [backport core/1.40] Stabilize nested subgraph promoted widget resolution
- #9577 — [backport core/1.40] Textarea stays disabled after link disconnect on promoted widgets
### Architecture References
- [ReactFlow Performance Documentation](https://reactflow.dev/learn/advanced-use/performance)
- [Designing a Dataflow Editor with TypeScript and React — Protocol Labs](https://research.protocol.ai/blog/2021/designing-a-dataflow-editor-with-typescript-and-react/)
- [ComfyUI Node 2.0 Blog Post](https://blog.comfy.org/p/comfyui-node-2-0)
┆Issue is synchronized with this [Notion page](https://www.notion.so/Issue-10002-Long-term-Rendering-Architecture-Roadmap-Hybrid-Canvas-Vue-Strategy-3256d73d36508172b994c39366e0affd) by [Unito](https://www.unito.io)
Contributor guide
Assessment
This issue has not been assessed yet.