Summary#
Add a dirty-bit system to the layout engine so that only modified subtrees are re-laid-out, instead of recomputing the entire layout tree on every frame.
Background#
Currently, layout() in crates/layout/src/lib.rs rebuilds the complete layout box tree and runs compute_layout() over every node on every render. For interactive pages with JS-driven DOM mutations, this is wasteful — most of the tree hasn't changed.
Acceptance Criteria#
- Add
dirty: boolflag toLayoutBox(or a separate dirty-bit map keyed by NodeId) - Define dirty propagation rules:
- When a DOM node's style changes → mark its layout box dirty
- When a DOM node is added/removed → mark parent dirty
- When
textContentchanges → mark containing box dirty - Dirty propagates upward: a dirty child marks ancestors dirty (up to the containing block)
-
compute_layout()skips clean subtrees — if a box and all descendants are clean, reuse cached dimensions/positions - Implement
mark_dirty(node_id)API callable from JS DOM bindings and style resolution - Handle cases that invalidate layout globally: viewport resize, font loading,
displaychanges - Incremental re-layout produces identical results to full re-layout (correctness invariant)
- All existing layout tests pass
- Add tests: mutate one node in a large tree, verify only the dirty subtree is re-laid-out (count layout invocations or add metrics)
Implementation Notes#
- A simple approach: keep the previous LayoutBox tree, diff against the new styled tree, and only re-layout dirty subtrees
- Alternatively: maintain a persistent layout tree that's mutated in-place, with dirty bits
- Margin collapsing complicates things: a dirty child may affect sibling margins. Consider re-laying-out the entire block formatting context when any child is dirty
- Flexbox/grid containers should re-layout all children if any child is dirty (their sizing is interdependent)
- Start conservative (mark more dirty than necessary) and optimize later
Dependencies#
None — independent of JS engine work.
Phase#
Phase 15: Performance