Component Rendering Model
The runtime (wgpu-html-ui::core::runtime) manages mounted components and selects one of three render paths per frame to minimize work.
MountedComponent
Each mounted component is tracked as a MountedComponent:
struct MountedComponent {
state: Box<dyn AnyComponent>, // Type-erased Component
last_node: Option<Node>, // Full output from last view()
skeleton_node: Option<Node>, // Skeleton with placeholder children
key: String,
needs_render: bool,
subtree_dirty: bool,
// ...
}
Three Render Paths
1. Clean Fast-Path
Condition: !needs_render && !subtree_dirty
Work: Nothing. The component's cached last_node is used as-is.
This is the common case when a component has no pending messages, props haven't changed, and no descendant needs rendering. Zero allocation, zero DOM manipulation.
2. Patch Path
Condition: needs_render && skeleton_node.is_some() && !props_changed
Work:
- Skip the parent's
view(). - Clone
skeleton_node(the DOM structure without children). - Only re-substitute children whose
needs_renderis true. - Update
last_nodewith the patched version.
This avoids expensive view() calls for the parent when only a single child changed. The parent's element structure is reused, and only dirty children are re-inserted.
3. Full Render
Condition: needs_render && (props_changed || no skeleton)
Work:
- Call
view(props, ctx, env)to produce a newEltree. - Reconcile child slots — match new children against existing mounted components by key.
- For matched children: call
props_changed()and optionallyupdate()+view(). - For new children: call
create(),mounted(), thenview(). - For removed children: call
destroyed(). - Build the output
Nodetree from theEltree and child components. - Cache
last_nodeandskeleton_nodefor future patch paths.
Keyed Children
Child identity is (String, TypeId):
// Auto-generated key (positional)
ctx.child::<MyComp>(props) → key = "__pos_{n}"
// User-specified key (stable across reordering)
ctx.keyed_child::<MyComp>("item-1", props) → key = "item-1"
Keyed children survive:
- Reordering: the runtime matches by key, not position.
- Insertion: new keys create new components; existing keys keep their state.
- Removal: keys not present in the new view are destroyed.
Without keys, positional children are rebuilt on reorder (destroy + create).
Message Processing
impl MsgSender<M> {
pub fn send(&self, msg: M) {
self.queue.lock().unwrap().push(msg);
(self.wake)(); // triggers re-render loop
}
}
MsgSender::send() enqueues a message and calls wake(), which the host maps to request_redraw(). The runtime's process() method drains all pending messages until the queue stabilizes:
while let Some(msg) = next_message() {
component.update(msg, props)?;
if component.needs_render() {
component.render();
}
}
Per-Component Caching
- last_node: The complete
Nodetree from the previousview(). Used directly on the clean fast-path. - skeleton_node: The
Nodestructure fromview()with all child component slots replaced by markers. Used for the patch path — the parent's structure is reused, only dirty children are substituted.
Both are invalidated on props_changed or when view() returns a structurally different tree.
Performance Characteristics
| Path | view() calls | Child reconcile | DOM writes |
|---|---|---|---|
| Clean fast-path | 0 | 0 | 0 |
| Patch | 0 (parent) | Only dirty children | Minimal |
| Full render | 1 | All children | Full rebuild |
For a typical interactive app (hovering, typing, clicking), most frames hit the clean fast-path or patch path. Full renders only happen on prop changes, initial mount, or structural DOM mutations.