Reconciliation
Introduction
Azul rebuilds the Dom from scratch on every Update::RefreshDom. The
tree returned by layout() is frozen. The next state change runs
layout() again and produces a new tree. The diff is what makes that
affordable.
The framework matches each new node against the most plausible old node, classifies what changed, and picks the cheapest path forward. A hover highlight on one button repaints one rectangle. Tree size doesn't matter.
Matching new to old
Every node in the new tree is one of four things:
- Stable. Matched against an old node at the same logical position. Old node id maps to new node id. Merge callbacks run. Focus and scroll state migrate.
- Moved. Matched against an old node at a different parent or sibling position. Same migration treatment as Stable.
- Created. No match. A Mount event fires.
- Destroyed. The old tree had a node here, the new tree doesn't. An Unmount event fires. The dataset drops.
For trees with stable sibling order, structural matching works without
any help. The third <li> inside a <div id="notes"> matches its
counterpart in the old tree. Its text content can change and it'll
still match.
For trees whose order can change (sortable lists, drag-and-drop,
virtualised scrolling), structural matching loses cursor position,
focus, and dataset state when items reorder. Those cases need a stable
identity. Set an AttributeType::Id on the node so the diff can match
across the reorder.
What does and doesn't trigger work
Different changes cost different amounts of work. The framework classifies each property change so it can pick the cheapest correct path:
- Repaint only. Color, background, opacity, transform, caret styling, object-fit, clip path. No layout pass runs. The display list updates and the affected pixels re-rasterise.
- Inline reshape. Font-style flip, vertical-align change inside a paragraph, letter-spacing update. Only the paragraph's text reflows. Block-level siblings stay where they are.
- Local resize. Border-width tweak, scrollbar gutter, padding inside a text container. The node's size recomputes. The parent may reposition subsequent siblings. No recursive relayout.
- Full subtree relayout. Flips of
display,position,float. Anything that changes the formatting context.
A :hover that only changes background-color lands in the repaint-
only bucket. A change to display: flex triggers a full subtree
relayout. The framework picks the right tier automatically.
Pseudo-class state changes (:hover, :focus, :active) go through
the same classification. A hover that flips just the background never
runs layout.
Lazy paint
Even after restyle decides „repaint only“, azul doesn't paint the whole window. It compares the new display list against the previous one and returns the rectangles that need re-rasterising. Everything outside those rectangles keeps the previous frame's pixels.
For a hover-state change on a button, this is constant-time work. The renderer rasterises maybe 100 by 30 pixels. The rest of the frame is a straight blit from the previous one.
A grow-only window resize feeds a specialised producer. Just the new right strip and bottom strip get rasterised. The existing pixels stay.
Per-frame flow
layout()returns a freshDom.- The framework matches new nodes against old. Lifecycle events (Mount, Unmount, Update) fire.
- Restyle promotes the changes to the smallest sufficient scope. If nothing layout-affecting changed, no layout pass runs.
- The display list rebuilds for the parts that changed.
- The renderer compares display lists and produces damage rects.
- Only those rects get rasterised.
For a hover-state change on a button, steps 3 to 5 are constant-time. Step 6 paints a single small rectangle.
Merge callbacks across the diff
For Stable and Moved nodes, the merge callback (if registered) runs
during the diff. It receives the old and new RefAny values. Heavy
resources (a video decoder, a GL texture, the cursor inside a focused
input) can move from the old dataset to the new one before the old
tree drops. See Merge Callbacks for the protocol.
For Created nodes, the Mount lifecycle event fires (no merge runs).
For Destroyed nodes, the dataset drops via Drop in the natural way
and the Unmount event fires.
Internals
The full algorithm (matching tiers, the property-change bitmask, the relayout-scope enum, damage-rect coalescing) lives in internals/events.md and internals/rendering.md.
Coming Up Next
- Datasets — Attaching state to a node for navigation and per-instance state
- Merge Callbacks — How widgets keep heavy resources across a layout rebuild
- Virtual Views — A node that materialises lazily, for infinite lists and embedded sub-DOMs