All posts

UI Is a Function You Cannot Afford to Call

From Sketchpad to self-adjusting computation: a model of what UI frameworks retain, invalidate, recompute, and preserve after an edit.

54 min read
  • ui
  • incremental computation
  • browsers
  • rust
On this page

Consider an ordinary piece of software that has lived long enough to become inconvenient: a table with one hundred thousand rows. Text wraps, cells turn into editors, selections cross the viewport boundary, and a translucent toolbar floats above the content. A background task replaces stale prices while a user composes text through an IME. Halfway through either operation, the window moves to a monitor with a different scale factor. Screen readers still need a coherent account of the table.

Now one visible cell changes from 19 to 20.

What should the computer do?

A lot of things have to change

It could forget everything and derive the entire interface again: run the application view, reconstruct every row, resolve every style, measure and arrange every box, shape every paragraph, generate every drawing command, rasterize every pixel, rebuild the accessibility tree, and present a new frame. I like how easy that machine is to reason about. I do not particularly want to wait for it.

Or it could rasterize and place only the replacement glyphs in an already composed image. That is probably wrong. A different advance width can move the rest of the line and widen the cell; layout may then alter a grid track, shift neighboring rows, and change the scroll extent. Focus, clipping, hit testing, and IME placement have to follow the new geometry, while the semantic text needs updating even if geometry does not move at all. If the floating toolbar uses a backdrop filter, its unchanged pixels still depend on the content underneath it.

The machine should discover which previous results remain valid, propagate the edit’s consequences, and reach the affected work without walking through everything else. That is a design problem, not a late optimization pass.

Rust UI comparisons often count widgets, compare binary sizes, note web support, inspect declarative syntax, or judge whether the counterexample looks pleasant. Those facts can help choose a library, but they say little about what happens after one cell changes. Following that edit through reactive evaluation, structure, style, text, layout, painting, GPU work, persistent pixels, hit testing, and accessibility exposes the architecture beneath the API.

“Immediate,” “retained,” “reactive,” and “declarative” are too blunt for the job. None says what the system retains, how it names retained things, how it discovers dependencies, where a precise edit becomes generic invalidation, or how much work it spends discovering a cache hit. Interactive-system history, browser engines, and incremental-computation research supply terms for those mechanisms. The value 19 will not survive the trip, but almost everything else should.

one cell: 19 → 20
reactive computa­tion
tree
style
text
layout
paint
raster
accessi­bility
presen­tation

The new string must be shaped. If its advance equals the old one, layout can cut off right there. The accessible text still changes even though no box moved.

stage does work conditional skipped

Change one visible cell. A paint-only edit stops early; a structural edit can reach every projection. Select an edit to trace its dependency cone.

1. The deceptively small function

Whenever the implementation details start to blur together, I come back to a very simple specification:

UI:InputStateObservableOutput\mathrm{UI} : \mathrm{InputState} \to \mathrm{ObservableOutput}

Expand the two sides and the simplicity disappears:

text
InputState = (
    application state,
    events and input-device state,
    time,
    loaded resources,
    viewport and device properties,
    user preferences,
    platform state,
)

ObservableOutput = (
    pixels,
    hit and event behavior,
    accessibility semantics,
    focus, selection, caret and IME state,
    cursor and drag state,
    clipboard, window and platform requests,
)

The definition is deliberately broader than ModelImage\mathrm{Model} \to \mathrm{Image}. A pixel-only model makes inconvenient bugs disappear on paper. A perfectly drawn text editor is still broken when its IME candidate window is anchored to yesterday’s caret, as is a popup drawn above its parent but hit-tested below it. After a list insertion, focus can silently transfer to a different logical row without changing a screenshot. Pixels are one observable projection, not the specification.

Call a cold evaluation of an app’s state Full:

Full(I)=O\mathrm{Full}(I) = O

Real UI engines do not call Full in the literal sense whenever something changes. They retain state between observations. Let Σ\Sigma denote all of that retained state and δI\delta I an input change:

Inc(Σ,δI)=(Σ,O)\mathrm{Inc}(\Sigma, \delta I) = (\Sigma', O')

The correctness target is:

OFull(IδI)O' \approx \mathrm{Full}(I \oplus \delta I)

Here \oplus applies a change to the old input, and \approx means observational equivalence. I do not require the incremental execution to allocate the same addresses, populate identical caches, choose identical internal node numbers, or perform the same amount of work as a cold execution. I require it to produce the same defined behavior at the observation boundary.

The small \approx hides plenty of retained-framework bugs because equivalence depends on when observation is allowed. A framework may expose intermediate state after every signal write, or only a stabilized result at the end of an event or animation frame. Accessibility queries and asynchronous effects make that boundary observable in ways a renderer cannot control.

1.1 The result is not one thing

A mature UI has no single “result.” It retains a stack of projections:

application state
reactive computations / component evaluations
document, widget or element identities
matched rules and computed style
text runs, shaped glyphs and line breaks
layout boxes and fragments
hit-test and accessibility projections
paint records, clips, transforms and stacking
renderer scene, GPU resources and raster tiles
composed pixels and presentation state
A mature UI retains a stack of projections. Each arrow carries both a value and a description of what changed.

An immediate-mode GUI may skip a conventional widget tree; a native wrapper delegates much of the stack to platform controls; a game engine folds painting into its render graph. Embedded toolkits often implement a smaller slice. The boundaries vary, but calling all persistent work “the render tree” hides the differences that matter here.

Suppose the application knows that row 48,193 changed its foreground color but tells the next stage only “the window needs redraw.” Locality has already been lost. Downstream code must rediscover it by comparing values, walking trees, checking cache keys, or intersecting bounds. That trade may be cheap, or it may dominate the frame; every arrow carries change information as well as a value.

The familiar UI labels describe different parts of this stack, not competing names for the whole machine.

1.2 Four labels that do not form a taxonomy

UI discussions often begin with pairs such as immediate/retained or declarative/imperative. I used to reach for those labels first. But they only work as long as each stays attached to the boundary it describes.

Immediate versus retained says something about the interface between two layers. Does the caller issue the current controls or drawing commands again, or mutate objects that persist at that interface? It does not tell us whether the implementation retains font data, interaction state, tessellations, layout results, textures, accessibility nodes, or pixels.

Declarative versus imperative says how desired behavior or structure is expressed. A declarative tree can be reconstructed wholesale every frame or compiled into precisely mutated retained objects. An imperative API can sit over an intensely incremental engine.

Reactive versus explicitly scheduled says how changes cause computations to run. A fine-grained signal can directly update one retained property and then request a full layout and repaint. Conversely, an event handler can imperatively mutate a node while the lower pipeline propagates a very precise set of obligations.

Incremental versus from-scratch describes how a layer maintains results across changes. It is not a binary property of a framework. A system can be incremental in text shaping, from-scratch in layout traversal, incremental again in raster caching, and full-surface in presentation.

Treating the labels as mutually exclusive species mistakes API syntax for runtime behavior. A rerun view can sit over deeply retained state, while a retained widget tree may still visit ten thousand clean siblings to find one dirty child. GPU acceleration and signal tracking live at similarly narrow boundaries: neither tells us what work the rest of the pipeline performs.

2. A quick history lesson

One tempting history runs from retained widgets through MVC, immediate mode, virtual DOMs, and finally fine-grained reactivity, as though each generation discovered its predecessor’s mistake. I believed some version of it for longer than I would like to admit. It is tidy, memorable, and mostly useless.

UI Concepts
UI concepts and how they relate to each other

Constraints coexist with event loops. Immediate interfaces sit over retained renderer state, declarative descriptions reconcile into object trees, and signal graphs mutate retained widgets. Browsers mix all of them in one engine. The branches never converged; each system keeps different state and restores consistency differently after a change.

2.1 Sketchpad: the drawing is already a database

Ivan Sutherland submitted the Sketchpad thesis in January 1963. Reading it now is slightly disorienting. Every few pages, an idea appears that software culture has since rediscovered under another name: graphical objects with identity, instances of master objects, hierarchy, direct manipulation, geometric constraints, and propagation of consequences.

Sketchpad
The original GUI

Sketchpad’s drawing was a stored, interconnected body of facts rather than an unstructured bitmap regenerated by a program. A symbol instance referred to a master. Constraints related geometric quantities. Editing one thing meant maintaining a network of derived relationships.

Calling Sketchpad a modern reactive UI framework would flatten six decades of engineering into a priority claim. Its design does, however, expose two questions present at the birth of interactive computer graphics:

  1. What makes a thing the same thing after an edit?
  2. Which other things must change when it changes?

Those are identity and dependency. Rendering, which I had expected to find at the center, enters later.

If two lines must remain perpendicular, moving one endpoint should restore that relation rather than replay a memorized sequence of drawing actions. The previous solution and its dependency structure matter; the new display is the visible consequence of a maintained model. Sketchpad’s constraints make that distinction very clear.

The original thesis, Sketchpad: A Man-Machine Graphical Communication System, is worth reading. The terminology is old, but the problem is not.

the drawing

P₁P₂ · drag me

what the system maintains

P₁P₂midpoint⊥ linerelation

stable object identity maintained relation re-solved while you drag

Left: a line constrained to remain perpendicular to another through its midpoint. Right: the dependency graph that maintains the relation. Drag P₂ to see which nodes are solved again.

2.2 Event loops and retained objects

Classic desktop toolkits made widgets persistent objects. They have parents and children, geometry, local state, event handlers, and perhaps native resources. An event loop dispatches platform events to those objects. A widget asks to be laid out or repainted; the toolkit coalesces the requests and eventually services them.

Identity now has an address, or at least a handle. Focus can refer to a widget. Mouse capture can outlive the callback that initiated it. An input method can communicate with a retained editor. A child can be inserted or removed without asking application code to narrate the entire interface again.

Give every widget in a retained tree a dirty: bool. If the root knows only that “something below me is dirty,” finding the widget may still require a full traversal. Propagate every layout invalidation to the root, and a leaf mutation can revisit the whole window; clear the backing surface on repaint, and retained widgets preserve no pixels. Retention alone doesn’t mean the underlying computation is incremental at every stage.

The label tends to merge three separate accomplishments:

  • keeping widget objects, which gives interaction continuity;
  • recording enough information to reach an affected widget, which gives localization;
  • preserving—or selectively reconstructing—the widget’s derived results, which gives work reuse.

A toolkit may do any one of these brilliantly and handle the others coarsely.

Traditional toolkits cover the entire range, from native peers and invalid rectangles to cached preferred sizes, separate measure/arrange invalidation, and independently transformed scene-graph layers. “Retained-mode toolkit” hides all of those choices behind one adjective.

2.3 FLEX and the old problem of reactive lifetime

Alan Kay’s 1993 retrospective, The Early History of Smalltalk, contains one of my favorite details in this history. In his account of FLEX, when expressions cached intermediate Boolean results and recorded dependencies on variables. When a dependency changed, the machinery could reconsider the relevant condition.

The resemblance to reactive dependency tracking is striking, but Kay also describes difficulty controlling how long this event machinery should remain active. Dependency discovery and dependency lifetime appear together.

Those concepts are still relevant in modern UI. Suppose a UI contains:

rust
if show_details.get() {
    label(move || expensive_state.get().summary())
}

When show_details becomes false, it is not enough to stop painting the label. The computation that read expensive_state should no longer be a live consumer. Its callback should not fire. Any asynchronous work owned by the branch may need cancellation. Context subscriptions, timers, accessibility nodes, focus targets, and cached resources need coherent lifetimes.

Those callbacks, subscriptions, timers, and cached resources make the reactive graph a graph of scopes as well as values. Dependency tracking is easy to demonstrate; owning the resulting lifetimes is harder.

2.4 MVC before the three-box diagram

Trygve Reenskaug’s December 1979 note on Models–Views–Controllers is refreshingly unlike the generic three-box diagram that later inherited the name. The model represents knowledge. Views are visual representations that ask questions of the model. Controllers connect user activity to the system. The motivating problem is not arranging source files into directories. It is coordinating several presentations and modes of interaction around shared knowledge.

The persistent problem is synchronized projections, not whether a project has put its controller in the correct directory.

A spreadsheet cell may simultaneously exist as domain data, formatted text, a visible grid item, an editable control, a formula dependency, an accessibility node, a selection participant, and a target for undo. These are not copies that can drift independently. They are projections with different update costs and different identities.

Modern architectures place the synchronization mechanism in different locations. It may look like observer callbacks into retained views, reducer-driven reconstruction, tracked signal reads, or incremental query maintenance. A browser spreads it across mutation and invalidation machinery. The labels differ; the obligation does not.

2.5 Constraints: incrementality inside one layer

Constraint systems take the maintained-relation idea seriously. Cassowary, developed for interactive UI layout, incrementally solves systems of required and preferred linear equalities and inequalities. When a constraint or edit variable changes, the solver starts with retained solver state rather than discarding the whole problem.

The Cassowary papers and project archive changed the way I think about “layout.” Layout need not mean recursively running a tree algorithm from a blank slate; its result can be the maintained solution of a changing relation.

Cassowary also reveals the limits of a local victory. An incremental constraint solver maintains geometry, not application structure or the downstream text, paint, and accessibility projections. The rest of the pipeline may still be broadly recomputed.

2.6 Immediate mode: forgetting at one boundary

An immediate-mode drawing API asks the caller to issue drawing commands for the current output. An immediate-mode GUI usually asks application code to issue its controls again during a pass:

rust
ui.heading("Orders");

for order in orders.iter() {
    ui.horizontal(|ui| {
        ui.label(&order.name);
        if ui.button("Cancel").clicked() {
            cancel(order.id);
        }
    });
}

A loop now describes the current rows, and a conditional describes a conditional control. Application code follows normal control flow instead of synchronizing a separate mutable widget graph; what appears on screen is what the current pass asked for.

It is common to summarize the trade as “immediate mode stores no state.” The phrase obscures actual implementations. Thierry Excoffier’s 2003 Zero Memory Widgets explored avoiding per-widget references and putting relevant data in application state, but even its provocative title does not claim that the entire machine literally remembers nothing. Omar Cornut’s living Dear ImGui paradigm note makes the relevant distinction explicit: immediate mode characterizes the application/library interface, not the absence of internal retention, a required refresh rate, or a particular renderer.

The call ui.button(...) may be immediate at the widget API boundary while the implementation retains active/hovered IDs, window state, font atlases, textures, tessellation caches, input queues, clipboard state, and GPU resources. The application may also provide an explicit stable ID when call order is insufficient.

Equally, an immediate interface does not tell us that every underlying operation is repeated. The library can hash an ID, look up retained state, early-out clipped regions, cache galley layout, preserve texture allocations, and perform partial platform updates. Or it can rebuild and redraw almost everything because that work is cheap enough for its target workloads. The paradigm permits both.

What does get reconstructed is information at a particular seam. The application repeats a description through control flow. If the framework wants continuity, it associates this pass’s calls with prior state using position, ID stacks, explicit keys, or other names. Immediate mode does not abolish identity; it makes identity recovery part of the interface contract.

2.7 Reconstructed descriptions: make the new world cheap to state

Elm-style view(model) and virtual-tree systems make a different bargain. Instead of mutating retained widgets directly, application code constructs a description of the desired interface:

rust
fn view(model: &Model) -> Element<Message> {
    column![
        text(format!("{} orders", model.orders.len())),
        keyed_list(
            model.orders.iter().map(|order| {
                (order.id, order_row(order))
            })
        ),
    ]
}

The description may be fresh. The UI need not be.

A reconciler compares the new description with retained state and maps it onto old identities, producing updates, moves, creations, and removals. React’s current documentation explains its user-visible identity rule through tree position, type, and keys; The Elm Architecture presents deterministic model/update/view composition. Specific runtimes differ enormously. Across them, application code can cheaply state a new world while the framework owns its continuity with the old one.

Application code describes a state instead of carefully issuing the mutations that produce it. The reconciler inherits the complexity.

Suppose the first row is inserted into an unkeyed list. A positional reconciler may associate old row state with the wrong logical data, or replace state for every following row. Stable keys let it map surviving descriptions to surviving nodes, but they do not tell us how far that correspondence travels. Component state may survive while the associated layout node, text object, paint record, or accessibility ID is replaced at the next architectural boundary.

I call such a boundary an identity cliff: a small semantic edit replaces retained identity and destroys reuse below that point.

rowdata keycomponentwidgetlayout boxa11y node
Grinding wheel#1#1#1#1#1
Bolt M8#2#2#2#2#2
Washer#3#3#3#3#3
Bearing#4#4#4#4#4
Belt#5#5#5#5#5

Identity follows the data key. The five surviving items keep every token and only the new row allocates new ones. This is the ideal that keys promise.

kept kept, but now on the wrong item replaced

Insert a row at the top and compare three identity strategies: positional matching, keyed matching, and keyed matching above rebuilt layout boxes.

A framework may rerun the whole view and compare a hundred thousand descriptions only to conclude that 99,999 retained nodes can be reused. The expensive work survives, but finding it is still linear: reconstruction has introduced discovery cost. Cheap descriptions can make that trade worthwhile, though it remains different from scheduling one affected computation directly.

2.8 Fine-grained reactivity: remember who read what

Fine-grained signal systems move dependency discovery into ordinary reads. The first implementation feels almost magical: the code looks like a plain read while the runtime quietly learns a graph. In schematic Rust:

rust
let price = Signal::new(19_u32);

let text = Memo::new(move || format!("{}", price.get()));

Effect::new(move || {
    price_label.set_text(text.get());
});

While evaluating the memo or effect, the runtime records the signal reads. A write marks or schedules the consumers. If control flow changes, reevaluation replaces obsolete edges. If a branch is destroyed, its owner disposes the computations.

Recorded dependencies spare the root view from rediscovering the changed consumer. The runtime has a path from price to the exact memo and effect that observed it, and can stop propagation when the memo’s output equals its previous value.

The example ends where the framework architecture begins: what does set_text mean?

Depending on the framework, set_text might:

  • mutate one retained property and propagate separate text, layout, accessibility, and damage obligations;
  • rebuild the label’s child subtree;
  • regenerate a virtual tree at the nearest component boundary;
  • mark the whole window dirty;
  • clear a canvas and redraw every visible item.

Fine-grained reactivity precisely answers “which application computation should run?” It does not automatically answer “which layout boxes should be visited?” or “which pixels should be reconstructed?” The answer has crossed into another change domain.

2.9 How the techniques combine

A modern system may let a signal trigger a reconstructed component, reconcile that component into retained widgets, lay one out with an incremental constraint solver, and feed an immediate paint stream into a retained renderer scene. The compositor may preserve pixels even though application descriptions were rebuilt. No tradition displaced the others because each layer chooses a different boundary where forgetting is cheap and memory pays for itself.

3. Browsers are scary

A browser is hostile to simple theories of UI. Arbitrary programs mutate a live document while style, several layout models, international editable text, scrolling and animation, hit testing, accessibility, and embedded content all remain coherent. Platform quirks are compatibility requirements, not bugs the engine can simply delete. Some stages are inevitably expensive, yet the browser still has to make an incoherent mixture of old and new projections unobservable.

It also cured me of saying “render” when I meant six different computations, or assuming that a renderer could supply the integration policy above it.

3.1 The pipeline is a stack of retained hypotheses

A deliberately simplified browser pipeline looks like this:

events / timers / resources
script checkpoint
DOM mutations
style invalidation
candidates
selector matching + cascade
computed style changes
box construction + layout
fragments / geometry
pre-paint + paint artifacts
display items / property trees
layerization / commit
scene and resource changes
raster + compositing
presentation

Side projections, fed from several stages at once: hit testing, scrolling, selection, caret and IME, accessibility.

A simplified browser pipeline. Each arrow names the change language one stage emits for the next.

This is a conceptual decomposition, not a current call graph shared by every engine. Blink, Gecko, WebKit, and Servo use different names, split points, ownership rules, and generations of architecture.

Nor are its boxes disposable phases through which raw values flow once. They hold retained hypotheses about the current document. For our table row, the engine remembers which selectors matched and which values were inherited, then keeps the resulting boxes, line breaks, fragments, transforms, and clips. Farther down, paint chunks correspond to display items and raster tiles claim to contain valid pixels. Every layer holds a different belief about the same row.

When an edit challenges those hypotheses, invalidation conservatively identifies which retained beliefs may no longer hold. A dirty flag is only one possible encoding.

3.2 Follow the 19

Put our troublesome table cell into a browser-shaped UI:

html
<div class="row" data-id="48193">
  <span class="name">Grinding wheel</span>
  <span class="price">19</span>
</div>

Suppose script changes the text node from 19 to 20. The mutation already names one text node and its new content. It may not change which selectors match the parent at all. Richer structural edits such as adding a class, changing an attribute, or inserting a sibling enter the style system differently. Inheritance and selector relationships can turn one local mutation into work on ancestors, siblings, or descendants.

The text engine shapes the new characters under the current fonts, language, direction, features, spacing, and scale, perhaps selecting different glyphs or fallback fonts. A changed advance can alter line breaks and intrinsic sizes. A new min-content contribution may then alter a flex or grid track; a taller line can move later rows and extend a scroll container. Measurements for those later rows may remain reusable even while their final positions shift.

Only then do we arrive at pixels. The old glyph ink must disappear and the new ink must appear, with both old and new bounds included if geometry moved. Decorations, selection, a caret, shadows, outlines, and clips can enlarge that region. Some affected tiles may be rerasterized while their neighbors survive. Even the unchanged floating toolbar is not automatically safe: if it samples the content behind it through a backdrop effect, it has a spatial dependency on the changed pixels.

The nonvisual interface changes alongside the visual one. The accessible text is new even if the replacement happens to have identical metrics and pixels; hit regions may move, and a focused editor may need a new caret rectangle and IME anchor. An engine need not perform all of that work for every text edit, but it must either propagate each consequence or prove it irrelevant.

TextChanged(node) cannot be lowered directly to Paint(rect), but neither should it become EverythingChanged before the engine examines the relevant dependencies.

Now compare three edits to the same row:

css
/* A: paint-like */
.price { color: red; }

/* B: layout-like */
.price { font-weight: 700; }

/* C: potentially compositor-like */
.row { transform: translateX(4px); }

The likely dependency cones differ, though these categories are not absolute. Color can stop at paint. Font weight may select metrically compatible or incompatible glyphs and therefore reach layout. A transform on a suitable retained layer can enter at the compositor, depending on retained compositing state.

CSS is brutal

Chromium’s dated but still instructive 2021 RenderingNG architecture series describes this kind of stage-skipping. Some changes enter the decomposed pipeline at a later stage instead of replaying every earlier one. The exact machinery evolves; the durable point is that a change’s type determines where it enters and how far it travels.

script
style invali­dation
match + cascade
layout
paint
layerize
raster + composite
presen­tation

Enters near paint. A computed value changes but no box geometry does, so layout is skipped. Layerization is only revisited if the change affects compositing decisions.

stage does work conditional skipped

Trace the three edits above. A enters near paint, B can reach layout, and C may remain in the compositor. A question mark means retained state determines whether the stage participates.

3.3 Style invalidation is dependency analysis under a language

CSS is a fine adversary for tidy incremental models: its apparent bag of visual properties is a program over document structure, state, environment, and inheritance.

Consider:

css
.table:has(.row.selected) .toolbar { opacity: 1; }
.row:nth-child(odd)               { background: var(--stripe); }
.table.compact .price             { font-variant-numeric: tabular-nums; }

Inserting one row can change :nth-child matches for many later siblings. Toggling selected can affect an ancestor’s :has(...) result and then a distant toolbar. Changing a custom property on an ancestor can change computed values for many descendants. The dependency graph exists whether or not it is materialized as ordinary graph nodes.

A style engine therefore needs more than cached computed-style objects. It needs a way to discover which nodes might need rematching or recascading after a mutation. Too narrow and the UI becomes stale. Too broad and a local edit produces a subtree walk. Selector invalidation is a domain-specific dependency analysis whose false negatives are correctness bugs and whose false positives are performance costs.

The distinction between rematching and recascading also applies outside browsers. If the set of matched rules remains stable but an inherited value changes, some systems can avoid selector work while updating computed values. If computed style changes only in a paint property, layout can remain valid. “Restyle” is too coarse to describe either path.

3.4 A layout tree is not a DOM with rectangles

The DOM expresses authored structure; layout operates on generated boxes and fragments. The convenient model of putting a rectangle on every DOM node fails in several ways:

  • an element may generate no box, while anonymous boxes can exist without an authored node;
  • one element can produce several fragments across lines, columns, or pages;
  • pseudo-elements contribute generated content;
  • a text node participates in line construction with neighboring runs;
  • positioned descendants depend on containing blocks that simple parent geometry does not capture;
  • intrinsic sizing may query descendants before final constraints are known.

Stable DOM identity does not automatically provide stable layout identity. The same authored node can survive while the system falls off an identity cliff one layer below it. If a layout pass rebuilds box objects wholesale, lower caches keyed by those objects lose their correspondence even when DOM nodes survive. Conversely, a layout engine may retain boxes and fragments while application components are reconstructed above it.

Servo offers a live Rust example. Its 2023 Layout Engines Report records an earlier architecture and its tradeoffs. Servo’s official March 2026 project update reports a move from dirty-root layout toward incremental box-tree layout, including reuse of cached fragments for independent formatting contexts. Two optimizations are at work:

  • a dirty root says where recomputation may begin;
  • reusable artifacts inside that region reduce what recomputation actually has to redo.

Finding a smaller root and doing less work within that root are independent dimensions.

3.5 Cache hits can still be expensive

Suppose layout is expressed recursively:

rust
fn layout(node: NodeId, constraints: Constraints) -> Size {
    let key = (node, constraints, node.style_generation());
    if let Some(size) = cache.get(&key) {
        return size;
    }

    let size = layout_uncached(node, constraints);
    cache.insert(key, size);
    size
}

A cache hit can eliminate expensive measurement while the caller still enters layout(root, ...) and recursively reaches cached nodes. Depending on where cache checks occur, the engine may inspect a large fraction of the tree just to collect hits.

The cache separates two costs that framework discussions often combine:

recomputation cost=work required to derive invalid valuesdiscovery cost=work required to find invalid or reusable values\begin{aligned} \text{recomputation cost} &\;=\; \text{work required to derive invalid values} \\ \text{discovery cost} &\;=\; \text{work required to find invalid or reusable values} \end{aligned}

A dirty-root index, a list of dirty descendants, a phase-level “nothing below needs layout” gate, or direct dependency scheduling can reduce discovery cost. A memo table mainly reduces recomputation cost. Both matter, and benchmark labels such as “layout time” often hide the distinction.

3.6 Painting is not rasterization is not presentation

“Redraw” has become one of my least favorite verbs in UI engineering. It sounds precise while allowing speaker and listener to imagine entirely different work.

Painting can mean generating abstract display items: fill this rounded rectangle, draw this glyph run, apply this clip, begin this opacity group. Rasterization converts suitable portions of those descriptions into pixels. Compositing combines rasterized surfaces or textures under transforms, clips, opacity, filters, and ordering. Presentation transfers or makes the composed result visible through a window-system surface.

Retention can stop at any of those boundaries:

  • paint records may survive while the renderer scene is regenerated;
  • rebuilt display items may still hit raster caches;
  • one tile may be rerasterized before a full-frame composition;
  • a persistent offscreen target may still be copied into every transient swapchain image.

Each boundary has its own retained state and its own delta protocol.

Blink’s current source-tree paint README describes display items and chunks, property trees, reuse, layerization, and raster invalidation. WebRender’s rendering overview distinguishes scene construction from viewport-specific frame building and renderer tasks. These are not interchangeable designs, but both show why “uses a GPU renderer” does not characterize a UI’s incremental behavior.

Clearing a damaged rectangle destroys every visible contribution inside it, including those from unchanged objects. Correct partial painting therefore needs two predicates:

text
record_changed(fragment)       // must regenerate its own paint description
overlaps_damage(fragment, D)   // must contribute to reconstructing D

The second set can include unchanged content. Effects make the geometry harder: blur expands an output neighborhood; shadows extend beyond layout bounds; transforms move old ink; backdrop filters read pixels behind the object; antialiasing requires conservative edges. Pixel damage is a spatial dependency analysis, not a synonym for logical dirtiness.

For an object that moves from old_bounds to new_bounds, the elementary conservative rule is:

rust
let damage = old_ink_bounds.union(new_ink_bounds);

A painter needs ink bounds, not just layout bounds. Ordering and overlap determine what must be replayed; effects can map local damage through read and output regions; sound occlusion tracking may reduce the work.

3.7 Side projections refuse to be side effects

Rendering diagrams often relegate accessibility and hit testing to small boxes at the edge. The resulting bugs reveal how deeply both depend on the main pipeline.

The accessibility tree is another retained projection, with stable identity, parent/child relationships, roles, properties, text, actions, focus, and geometry. It is easy to omit from architecture diagrams. A platform adapter may retain the complete tree and accept incremental updates, as AccessKit does. An immediate-mode authoring API remains compatible with that design if the toolkit supplies stable semantic IDs across passes.

If a keyed row keeps its component state but receives a fresh accessibility identity on every update, assistive technology can experience focus churn or incoherent events. If geometry changes but semantic bounds do not, navigation and visuals disagree. Accessibility makes internal identity externally observable.

Hit testing is similarly derived from geometry, transforms, clips, ordering, pointer-events semantics, and sometimes custom shapes. Rebuilding a visual layer without updating the hit-test projection violates our O' ≈ Full(...) target even if every pixel is perfect.

Selection, caret, and IME state are more entangled still. Text shaping and line breaking determine caret positions. Scroll and transforms determine screen coordinates. Platform APIs may query or display that state on schedules the framework does not completely control.

Pixels, hit-test results, and platform semantics therefore need a shared, explicit consistency boundary.

3.8 Feedback: the pipeline is not a DAG

Geometry-dependent behavior breaks the neat downward arrows.

A container query chooses style from a size that style itself helps determine, creating the most direct loop between style and layout. Scrollbars can create another by reducing the width that decides whether their content overflows. Font loading and geometry observers feed new information into the same cycle, while intrinsic sizing may run exploratory child layouts before final placement. The tidy pipeline has become an iterative schedule.

stylelayoutobserved geometrymutationcomputed valuessizes, positionscallbacks runwrites state
Geometry-dependent behavior turns the pipeline into a cycle. The framework's semantics determine whether it reaches a fixed point and what can be observed beforehand.

There may be a stable fixed point. There may be a cycle. There may be several possible stable states depending on evaluation rules. Real systems establish phases, suppression rules, iteration limits, and deferral policies. The meaningful correctness statement must name the observation point:

  • consistent immediately after every write;
  • consistent after an event or stabilization transaction;
  • consistent at a bounded end-of-frame point;
  • eventually consistent over later frames, with intermediate states constrained somehow.

An acyclic dependency graph, a phased feedback system, and a scheduler that reruns tasks until exhausting a budget have different semantics even if their pipeline diagrams look alike.

3.9 Two requirements: equivalence and proportional work

Mozilla’s historical Dynamic Change Handling document states two goals quite clearly: rendering after a dynamic DOM change should agree with rendering the resulting DOM from scratch, and processing for a change with small effect should take proportionally little time. The implementation details and TODOs in that document are historical, not a description of current Gecko, but the goals still state what modern browser aim for.

Meeting both requirements takes more than caches and dirty bits. The surrounding architecture needs:

  • stable correspondences between retained representations;
  • conservative translations between their change languages;
  • a way to reach affected work and stop when results remain equivalent;
  • phase and flush rules that define when results may be observed;
  • explicit semantics for feedback;
  • spatial reasoning for pixels;
  • coherent updates to accessibility and input projections.

Browser engines supply concrete examples of retention, localization, propagation, cutoff, demand, and consistency. Incremental-computation research lets us separate them instead of using “incremental UI” as a compliment.

4. Vocabulary for the life of an old result

Take an ordinary program:

y=F(x)y = F(x)

After an edit δx\delta x, the simplest implementation computes F(xδx)F(x \oplus \delta x) from scratch. An incremental implementation retains some metadata or trace τ\tau:

Adjust(τ,δx)=(τ,y)\mathrm{Adjust}(\tau, \delta x) = (\tau', y')

and aims for:

yF(xδx)y' \approx F(x \oplus \delta x)

The formulation covers spreadsheet recalculation, incremental compilers, database view maintenance, build systems, reactive signals, layout caches, and damaged-region painting. It is broad enough to flatter a hash map wrapped around an expensive walk. Precision comes from following the life of an old result.

4.1 From remembering to doing

The life of an old result passes through six distinct mechanisms:

  1. Retention keeps a prior input, output, intermediate value, identity, or dependency available. A widget object and a backing store full of last frame’s pixels are both retained state, despite living at opposite ends of the pipeline. Retention creates the possibility of reuse, but a preserved value can still be stale, unreachable, or expensive to validate.

  2. Change detection tells the system that an input may differ. Signal versions, mutation records, equality checks, dirty bits, hashes, and generation counters do so with different precision and cost. A structured edit such as Insert { index, value } carries more information than any of them because it also says what happened.

  3. Dependency localization answers where that change might matter without interrogating everything else. Reverse signal edges, dirty-child summaries, spatial indexes, and build databases are specialized answers to this question. A root dirty bit is one too, although its answer is simply “somewhere below the root.”

  4. Change propagation brings the affected work up to date and forwards its consequences. When dependencies are dynamic, propagation also repairs the graph: a computation that now reads b instead of a must remove one edge as it establishes the other.

  5. Memoization and reuse preserve work when identity and relevant inputs still match. Reuse may happen before execution through a cache lookup or after it through an early cutoff. Either way, it is only as sound as the dependency signature that justifies the match.

  6. Demand allows a stale result to remain stale until an observer requires it. A collapsed subtree may not need layout, and an unobserved memo may not need evaluation, but visibility is only a rough proxy: offscreen content can still affect scroll extent or accessibility.

The mechanisms overlap without implying one another:

  • a cache gives us retention and possible reuse, not a route to the changed consumer;
  • a dependency graph may localize stale nodes but still propagate eagerly;
  • a virtual tree can preserve identity while rediscovering it through a broad evaluation;
  • a damage rectangle localizes pixels while knowing nothing about application state.

4.2 One function, six very different machines

Consider:

rust
fn total(orders: &[Order]) -> Money {
    orders.iter().map(|o| o.price * o.quantity).sum()
}

When one quantity changes, several implementations are possible.

  1. Recompute the fold over every order.
  2. Cache each row subtotal, but walk every row to check cache keys.
  3. Maintain a segment tree, updating one leaf and O(log n) ancestors.
  4. Represent the edit as ChangeQuantity { id, old, new } and adjust the total by the difference.
  5. Maintain a dynamic dependency graph in which the observed total depends on row computations.
  6. Mark affected nodes stale and recompute the total only when some observer demands it.

The return value is identical in every case; retained state, update language, discovery cost, and demand semantics are not. UI frameworks make such choices at several layers simultaneously.

5. Self-adjusting computation

The problem can be formalized as Self-adjusting computation. Coming from UI engineering, I initially found the terminology more distant than the ideas. The classical work by Umut Acar and collaborators studies programs that retain execution information and adjust their output when inputs change. The foundational references include Adaptive Functional Programming, Acar’s 2005 thesis, and A Consistent Semantics of Self-Adjusting Computation.

The notation varies between systems; several ideas transfer directly to UI work.

5.1 Modifiables and the dynamic dependence graph

An adaptive computation reads changeable inputs through tracked locations—often called modifiables in this literature. During evaluation, the runtime records which computation depends on which read. The resulting dynamic dependence graph, or a trace encoding equivalent information, explains where a later input change can matter.

Schematic pseudocode looks familiar to anyone who has implemented signals:

rust
fn read<T: Clone>(source: &Modifiable<T>) -> T {
    if let Some(reader) = CURRENT_COMPUTATION.get() {
        source.add_dependent(reader);
        reader.record_source(source.id());
    }
    source.value.clone()
}

The difficult semantics begin after read, when a value changes:

  • Which dependents are scheduled, and in what order?
  • When may propagation stop?
  • How does reevaluation remove stale outgoing edges?
  • What happens when control flow reshapes the graph during traversal?
  • Can an effect observe a mixture of old and new values?
  • How does the trace disappear when its piece of UI does?

Ordinary conditionals, loops, and function calls shape a graph whose dependencies are discovered during execution. Its lifetime and scheduling are therefore semantic concerns rather than housekeeping, the same difficulty Kay described for FLEX.

5.2 From-scratch consistency

The central promise is agreement with fresh evaluation. Fast updates are a possible consequence, not the correctness criterion.

If an adaptive program begins with input x, computes a result and trace, receives an edit to produce x', and propagates that edit, the resulting value should equal—or be appropriately equivalent to—the result of running the original program from scratch on x'.

For UI, I would state this as:

observe(adjust(Σ,δI))observe(full(IδI))\mathrm{observe}(\mathrm{adjust}(\Sigma, \delta I)) \approx \mathrm{observe}(\mathrm{full}(I \oplus \delta I))

The observe function is essential because internal equality is neither necessary nor sufficient. A cold execution may choose different node IDs and cache contents without changing the UI. Conversely, matching final pixels can conceal divergent focus or accessibility events. Floating-point rendering needs an explicit comparison rule, while asynchronous I/O has to be controlled rather than blindly repeated by a “cold reference” execution.

From-scratch consistency forces “the same UI” to include an observation relation. It also suggests a practical test: given the same logical input, compare an incrementally maintained UI with a freshly constructed reference at chosen quiescent boundaries:

rust
#[test]
fn incremental_result_matches_cold_result() {
    let mut inc = App::new(initial_state());
    let mut model = initial_state();

    for edit in generated_edits() {
        inc.apply(edit.clone());
        model.apply(edit);

        inc.stabilize();
        let cold = App::new(model.clone()).stabilized();

        assert_equivalent(inc.snapshot(), cold.snapshot());
    }
}

The snapshot should contain more than a screenshot: semantic tree, hit-test answers at sampled points, layout geometry where public behavior depends on it, focus/selection state, and emitted platform requests as appropriate.

5.3 Trace stability

Incremental performance depends on how much the execution trace changes, not just on the apparent size of the input edit.

Suppose filtering one row flips a branch that changes the structure of a list. A one-bit input change can replace a large computation trace. No general-purpose runtime can reuse computations whose logical counterparts no longer exist unless the program supplies a stable correspondence at a different granularity.

Call this trace stability: how similar is the new dynamic computation to the old one under a small input change? UI keys then become part of the incremental algorithm rather than a reconciliation convenience. Stable entity IDs, keyed sequences, persistent nodes, and nominal names all try to preserve the portions of a trace worth reusing.

One changed field makes the hundred-thousand-row table easy only if the system can associate the changed row and all surviving rows with their previous computations and artifacts.

5.4 Early cutoff

Suppose a signal changes but a derived result remains equal:

rust
let width_class = Memo::new(move || {
    if window_width.get() < 800 { Narrow } else { Wide }
});

Changing width from 1200 to 1190 invalidates the memo, but its output stays Wide. If dependents observe only the enum, propagation can stop there.

The equal enum permits an early cutoff, converting input instability into downstream stability. The equality relation must match observation. If a memo returns f32 geometry, bit equality, approximate equality, and layout-equivalence have different semantics. If a style object compares only inherited fields, non-inherited dependents cannot use that cutoff.

An engine can therefore have many cutoff relations, each appropriate to a stage’s output contract.

5.5 Demand and Adapton

Classical eager propagation is not always desirable. If a stale computation has no current observers, updating it immediately wastes work. Adapton combines memoization with demand-driven incremental computation. Mutations dirty dependencies; demanded computations are brought up to date when forced; reevaluation repairs dependency edges; unchanged observed values can cut off further work.

Several UI cases map directly onto demand:

  • a background tab may postpone paint construction;
  • a virtualized row may exist without a paragraph;
  • a tooltip may remain stale until somebody asks to show it.

“Not visible” is not synonymous with “not demanded.” An offscreen item may affect scroll extent, and an invisible live region still matters to accessibility. Demand belongs to a projection, not to a single visibility bit.

A UI therefore needs distinct consumers rather than one visibility bit: layout needs an intrinsic measure; hit testing needs geometry in an active region; accessibility needs semantic nodes; paint needs fragments intersecting damage; presentation needs a composed image.

5.6 Names are part of the algorithm

If old work is to be reused after structure changes, the system must decide which new computation is the continuation of which old computation. This sounds philosophical until a text field jumps to the wrong row. It is not always derivable from values.

Nominal Adapton studies incremental computation in which names and namespaces control reuse across executions. UI keys are not automatically an implementation of Nominal Adapton, but both make naming part of the incremental algorithm: a name changes what reuse means.

Consider an unkeyed list:

rust
for item in items {
    row(item);
}

The implicit name may be the dynamic call path plus loop position. Insert at index zero and every later position now describes different data. If state follows position, it moves to the wrong semantic item. If the reconciler detects value differences and replaces everything, correctness may survive but reuse collapses.

Now introduce a key:

rust
for item in items {
    keyed(item.id, || row(item));
}

The name can remain stable under insertion and reorder, but only within a naming discipline:

  • Is the namespace a sibling list, an owner, or the whole application?
  • What do duplicate names and reuse after removal mean?
  • Does the name preserve component state, layout artifacts, or both?
  • Does moving it between parents preserve or replace its identity?
  • Can an asynchronous callback hold a name after its underlying identity is recycled?

Rust’s ownership model does not settle any of this for a framework. I wish it did. Generational arenas, scoped owners, handles, Arcs, and typed IDs provide the mechanisms; the UI’s identity semantics remain a design decision.

6. Four neighboring theories

UI engines mix several styles of incrementality. Problems that UI discussions often treat as implementation trivia already have sharper names in neighboring fields.

6.1 Incremental lambda calculus: can we differentiate a program?

In ordinary calculus, a derivative describes how output changes with input. Incremental lambda-calculus work asks for a program transformation that maps input changes to output changes. A Theory of Changes for Higher-Order Languages develops change structures and derivatives for higher-order programs.

Applied to UI, the shape is:

f:InputOutputDf:InputΔInputΔOutput\begin{aligned} f &\;:\; \mathrm{Input} \to \mathrm{Output} \\ Df &\;:\; \mathrm{Input} \to \Delta\mathrm{Input} \to \Delta\mathrm{Output} \end{aligned}

Could ClassAdded("warning") produce the exact computed-style delta without reevaluating style(node)? Could a child-size delta become a geometry patch without laying out the container again? At the next boundary, could fragment deltas become paint-record and damage deltas rather than a rebuilt scene?

Real UI engines mostly hand-write these derivatives. A property classifier lowers style changes into obligations; a sequence reconciler lowers collection changes into structural patches; a layout algorithm propagates changed measures; a fragment diff converts spatial change into damage. The stages differ, but each translates a richer input edit into the delta language understood below it.

An invalidation table is not an automatically derived, formally proven derivative. It is a conservative change translator maintained by engineers. Thinking of it as a hand-written derivative still sharpens code review: is it complete? A missing consequence is a correctness bug; an unnecessary one costs performance.

6.2 Database view maintenance: preserve the delta’s structure

A database query produces a derived view of changing collections. Incremental view maintenance avoids recomputing the full query when rows are inserted, removed, or updated. The input change is not merely “the table is dirty.” It carries multiplicity, keys, and changed tuples.

Differential dataflow generalizes this idea to collections of differences at logical times, including iterative computations. DBToaster’s higher-order delta work explores maintaining auxiliary views so updates can be processed rapidly.

UI systems benefit from preserving structured deltas while they remain meaningful.

rust
enum ListDelta<T> {
    Insert { index: usize, value: T },
    Remove { index: usize },
    Move { from: usize, to: usize },
    Update { index: usize, change: T::Change },
    ReplaceAll(Vec<T>),
}

Insert { index: 3, ... } contains more actionable information than list_changed = true. Because the edit names both operation and position, layout can preserve earlier measurements while virtualization, accessibility, and selection translate their own retained state from the same fact. Once widened to generic dirtiness, that information must be rediscovered or is simply lost.

An ordered, stateful, effectful UI is not a relational query. Focus and lifetime care about sequence and identity; painting cares about spatial overlap and z-order; feedback and asynchronous completion complicate logical time. What carries over from databases is the importance of the change algebra itself.

6.3 Build systems: visiting and rebuilding are different

In a build system, a keyed task produces an artifact from dependencies that may be declared or discovered. The scheduler decides which tasks to visit; the rebuilder then decides whether a visited task must run, using dirty bits, timestamps, hashes, or dependency traces. Build Systems à la Carte separates those roles into a framework for understanding different strategies.

The UI translation is direct:

Build systemUI system
target keynode, component, paragraph, or artifact identity
taskcompute view, style, layout, or paint record
dependencysignal read, ancestry, constraint, font/resource, spatial overlap
schedulerevent loop, reactive queue, dirty-tree walker, frame phases
rebuilderdirty bit, equality cutoff, cache-key validation
artifact storeretained tree, layout cache, scene, texture, pixels

A build can avoid recompiling every source file yet still scan a huge dependency graph. Likewise, a UI can achieve near-perfect cache hit rates while spending its time visiting nodes and validating those hits. Once you see the distinction in cargo, it is hard to unsee it in layout code.

For a one-row change, we need counters for both rows_remeasured = 1 and rows_visited_to_discover_that = 100_000. Reporting only the first congratulates the cache for work the scheduler failed to avoid.

6.4 FRP: change has time semantics

Functional reactive programming brings time-varying values and discrete events into the programming model. Conal Elliott and Paul Hudak’s Functional Reactive Animation is a foundational reference; the later survey of reactive programming maps a broader design space.

Practical UI “signals” inherit some of this vocabulary, but a signal library, an FRP system, and self-adjusting computation are not synonyms. To characterize a concrete runtime, ask:

  • Are values pushed to dependents, pulled on demand, or both?
  • Are dependencies static or discovered from reads?
  • Are writes applied immediately or batched?
  • When do effects run relative to computed values?
  • Can observers see glitches: inconsistent combinations of new and old upstream values?
  • How are cycles diagnosed or settled?
  • What is the ownership scope of a subscription?
  • Does equality stop propagation?
  • Can a stale computation remain unevaluated?

Consider:

rust
let celsius = Signal::new(20.0);
let fahrenheit = Memo::new(|| celsius.get() * 9.0 / 5.0 + 32.0);
let label = Memo::new(|| format!("{} °C / {} °F", celsius.get(), fahrenheit.get()));

If setting celsius first reruns label and only then updates fahrenheit, the label can briefly observe a new Celsius value with an old Fahrenheit value. A topologically ordered stabilization or transactional observation boundary prevents that glitch. In a pure text label the intermediate value may never be painted. In an effect that sends a network request, an intermediate observation can escape.

Time semantics become part of UI correctness whenever effects, animation, input, or platform callbacks can observe propagation.

6.5 Specialized incremental computation

General-purpose theories often represent arbitrary dynamic dependencies. UI engines know more about their problem, and it would be wasteful not to exploit that structure.

  • A reactive runtime records value-read edges.
  • A retained tree uses ancestry and dirty-child summaries.
  • A style system uses property classes and selector invalidation.
  • A text system uses keys for content, font selection, shaping, width, and breaking.
  • A layout tree caches measurements under constraints and propagates intrinsic-size changes.
  • A paint system uses fragment identity, old/new ink bounds, ordering, and spatial indexes.
  • A renderer uses resource generations, tiles, passes, and persistent surfaces.

These need not form one universal dynamic dependence graph. They are specialized representations connected by change translators.

Specialization can be more compact and efficient than recording every primitive read. A single NEEDS_RESHAPE bit can summarize a known family of dependencies, while lower stages choose representations that fit trees, regions, or renderer resources.

In exchange, every piece of domain-specific metadata carries a proof obligation. Misclassifying a glyph-affecting property as paint-only leaves stale text; the same omission at a spatial or accessibility boundary leaves old pixels or semantics.

In a generic tracked computation, missing dependencies usually come from untracked reads or effects. In a specialized UI pipeline, they also come from incorrect translation between change languages.

7. A model for comparing UI architectures

The comparison model below is not a mechanized semantics, and a Greek letter cannot rescue an imprecise implementation claim. Its job is to keep distinctions that prose tends to blur while allowing frameworks with different terminology to sit next to each other.

7.1 The retained-state vector

Let the retained state of a UI be a vector:

Σ=(A,R,D,C,T,B,F,H,Q,P,G,X)\Sigma = (A, R, D, C, T, B, F, H, Q, P, G, X)

where:

SymbolRetained state
Aapplication and platform input state
Rreactive subscriptions, scheduled computations, owners
Ddocument, component, element, or widget identities
Cmatched rules, computed styles, inherited data
Ttext analysis, font selection, shaping, lines, editing state
Blayout tree, constraints, measurements, geometry
Ffragments, transforms, clips, stacking, spatial bounds
Hhit-test, accessibility, and other semantic projections
Qpaint records or display-list ranges
Prenderer scene, pass plan, uploads
GGPU resources, atlases, cached targets
Xcomposed pixels and presentation state

No framework needs to expose these exact letters or even separate every entry. In some architectures, text layout lives inside widgets; in others, fragments are produced directly into a scene. Native controls move much of the vector behind an operating-system API. The model asks which artifacts survive a frame and where their identities come from.

“Framework X is retained” now requires a follow-up: retained where? It may retain D, B, and G but reconstruct Q. It may reconstruct D descriptions while retaining a parallel widget-state tree. It may retain almost everything below P while rebuilding application descriptions above it.

Incrementality is therefore a vector too.

7.2 Every stage consumes a change language

For stage ii, write:

adjusti:Si×ΔiSi×Δi+1\mathrm{adjust}_i : S_i \times \Delta_i \to S_i' \times \Delta_{i+1}

The stage receives retained state and an input delta, updates itself, and emits a delta meaningful to the next stage.

The change domains might contain:

rust
enum ReactiveDelta {
    SignalsChanged(SmallVec<SignalId>),
    TaskReady(TaskId),
    TimeAdvanced(Instant),
}

enum StructureDelta {
    SetProperty { node: NodeId, property: PropertyId },
    Insert { parent: NodeId, index: usize, child: NodeId },
    Move { child: NodeId, new_parent: NodeId, index: usize },
    Remove { node: NodeId },
    ReplaceSubtree { root: NodeId },
}

bitflags::bitflags! {
    struct Obligation: u16 {
        const REMATCH_STYLE  = 1 << 0;
        const RECASCADE      = 1 << 1;
        const RESHAPE_TEXT   = 1 << 2;
        const REBREAK_TEXT   = 1 << 3;
        const RELAYOUT       = 1 << 4;
        const REHIT_TEST     = 1 << 5;
        const REBUILD_A11Y   = 1 << 6;
        const REPAINT        = 1 << 7;
    }
}

struct FragmentDelta {
    id: FragmentId,
    old_ink: Option<Rect>,
    new_ink: Option<Rect>,
    paint_record_changed: bool,
    transform_changed: bool,
}

These enums are of course just illustrative. The point is that generic “dirty” terminology conceals the difference between SignalsChanged, SetProperty, RELAYOUT, and a damage rectangle. Translating one into another is an architectural operation.

Call a translator sound when it never omits downstream work that could alter an observable output. Soundness permits false positives:

actual dependency conescheduled dependency cone\text{actual dependency cone} \subseteq \text{scheduled dependency cone}

False positives do extra work, whereas false negatives leave observable state stale. Optimization narrows the conservative cone without allowing the inclusion to fail.

7.3 The information-loss perspective

Follow the edit until a rich delta becomes a poorer one:

QuantityChanged(order_id = 48193)
effect scheduled for PriceLabel(48193)
SetText(node = 92017, old = "19", new = "20")
RESHAPE | REBREAK | RELAYOUT | REPAINT | UPDATE_A11Y
layout root dirty
window dirty
Follow the information loss. Each step describes more possible consequences and less of what is actually known.

Widening at layout root dirty may be inherent because the changed intrinsic width participates in a global constraint. It may instead reflect the only invalidation API exposed by the layout library. Even window dirty may be harmless when scene generation is cheap and raster caches are strong. The diagram identifies where to investigate; measurement must grade the choice.

A framework may preserve different amounts of information for different edits:

EditPath through the pipeline
color changerepaint(node) → damage(old ∪ new ink)
text changereshape → relayout(ancestors) → repaint
viewport resizerelayout(root) → broad damage
compositor scrolltransform update → expose/raster missing tiles
font database editinvalidate many shape caches → broad layout

Broad work is appropriate when the semantic dependency cone is broad. Ask whether meaning requires it, a component contract imposes it, or information was discarded at a seam.

7.4 Discovery is part of the complexity

Let PP be the paths or candidates visited to locate work, and MM the retained computations that actually miss their cache or require reevaluation. A more honest frame-cost decomposition is:

T=Tschedule+Tlocalize(P)+Trecompute(M)+Tassemble+TpresentT = T_{\text{schedule}} + T_{\text{localize}}(P) + T_{\text{recompute}}(M) + T_{\text{assemble}} + T_{\text{present}}

Use the equation for accounting, not performance prediction. Constants, cache behavior, parallelism, GPU synchronization, and allocation still matter; the decomposition just prevents one impressive cache statistic from making the rest of the frame disappear.

For the table edit, record both scheduled work and work actually performed:

CounterFor the table edit
effects run1
component descriptions rebuilt1 or 100,001?
nodes reconciled0, 1, or 100,001?
dirty paths visiteddepth of row, or whole tree?
style matches / cascadeswhich nodes?
layout nodes enteredwhich nodes?
layout cache misseswhich nodes?
paragraphs shaped1?
paragraphs line-broken1, or the affected column?
paint records regenerated1, or the visible subtree?
unchanged records replayedhow many overlap damage?
primitives encodedlocal, or full scene?
damaged pixel areaold/new ink, or full surface?
bytes uploadedglyphs, buffers, or everything?
presentation copypartial, or full surface?

Wall-clock time remains essential, but these operation counts explain it. They also survive hardware changes better than one benchmark duration.

7.5 Cache keys are compressed dependency declarations

Suppose a text layout cache uses key K(x)K(x) for result shape(x)\mathrm{shape}(x). Reuse is sound when:

K(x)=K(x)shape(x)shape(x)K(x) = K(x') \implies \mathrm{shape}(x) \approx \mathrm{shape}(x')

The key might include content, font stack, font size, features, language, direction, scale, and font-database generation. Line breaking adds available width and wrapping/alignment options. Paint color should not be in a shaping key if it cannot affect the shaped result; putting it there creates false misses. Leaving language out may create stale results.

Every cache key is a compressed dependency declaration. Reviewing its fields reveals more than its hit rate alone: an explicit tuple exposes the claimed dependencies. Generations compress a large resource set at the price of invalidating its consumers together, while hashes compress further by adding a collision assumption. Pointer identity is only meaningful when the mutation rules preserve the immutability it implies.

Deriving hashes and invalidation classifications from one property definition reduces duplicated bookkeeping without proving that the classification is correct.

“Uses caching” is therefore not enough to characterize performance. Ask:

  • what identity indexes the cache;
  • which inputs form the key;
  • when invalidation reaches it;
  • whether it is demand-driven;
  • what traversal is needed to query it;
  • what eviction does to worst-case behavior;
  • whether a hit preserves only a value or also downstream identity.

7.6 Identity is a relation between executions

Incremental reuse needs a correspondence MM between entities in the old execution and entities in the new one. In less formal language, the framework needs to know which old thing a new description refers to.

Different systems construct M differently:

Identity schemeUsually stable underTypical cliff
call order / implicit pathidentical preceding control flowinsertion shifts later state
type plus sibling positionlocal property changesreorder or type change
explicit keyinsertion and reorder with unique stable keyscollision, reuse, parent move
retained object handlein-place mutation during object lifetimereconstruction loses mapping
generational entity IDarena mutation and safe deletionnew generation breaks stale handles
nominal computation nameexplicitly named structural editsname-discipline violation

A real framework commonly has several identities:

  1. semantic identity — the application’s “same order” or “same editor,” often exposed to accessibility and selection;
  2. structural identity — the component/widget/node used for lifecycle and event routing;
  3. render identity — layout boxes, fragments, paint records, textures, and scene objects used for work reuse.

They need not be identical. They do need sound translations.

Imagine a keyed text editor row moving from index 500 to index 2. Component state follows the data key, so the edit buffer survives. The layout engine, however, receives a replaced subtree and discards every box. The accessibility adapter assigns fresh IDs. The application test passes because the edit buffer is intact, while layout performance and screen-reader focus regress. “Keys preserve state” described only one boundary.

7.7 Lifetime is identity over time

Dynamic UI structure turns identity into ownership.

When a conditional branch disappears, all of the following may need to happen:

  • unsubscribe reactive reads;
  • cancel or detach asynchronous work;
  • release pointer capture;
  • move focus according to defined rules;
  • retire accessibility nodes and emit updates;
  • destroy widget-local state;
  • invalidate layout ancestry;
  • damage old ink bounds;
  • eventually reclaim text, renderer, and GPU resources.

The order can be observable. An async completion racing with removal may address a recycled node; delayed effect cleanup can leave old and new subscriptions live together. Even immediate reuse of an accessibility ID risks making the platform interpret replacement as continuity.

Rust prevents many classes of memory unsafety, but it does not automatically provide semantic lifetime safety. Frameworks still choose between lexical scopes, tree ownership, reference counting, arenas, generations, deferred destruction, weak handles, and cancellation tokens.

The code investigations therefore track ownership of:

  • component/effect scopes;
  • widget state;
  • layout nodes;
  • asynchronous tasks;
  • accessibility identities;
  • cached renderer resources.

Whether logical death reaches every retained projection promptly and safely matters more here than garbage collection versus ownership.

7.8 Logical and spatial changes are different algebras

Tree invalidation and pixel damage are related but not isomorphic.

Logical invalidation follows semantic dependencies and structure: a child’s intrinsic width may invalidate ancestors; an inherited style change may flow to descendants; a focus change may update two nodes and a platform focus handle.

Spatial damage follows coverage and read dependencies: remove old ink, draw new ink, replay unchanged objects that overlap cleared pixels, expand for filters, map through transforms, and account for retained surfaces.

Concrete cases expose the mismatch:

  • an accessibility change is logically dirty but may damage no pixels;
  • an unchanged node may need replay because it overlaps damaged pixels;
  • a compositor transform can move content without rebuilding its paint record;
  • an offscreen change may alter scroll extent without touching visible pixels.

Encoding all of these with one dirty: bool forces either incorrectness or broad work. I have written plenty of useful dirty bits; this one is simply being asked to represent several different algebras of change.

The obligations can be represented as a product:

rust
struct Dirty {
    semantic: SemanticObligations,
    structural: StructuralObligations,
    text: TextObligations,
    layout: LayoutObligations,
    paint: PaintObligations,
    damage: Region,
    renderer: RendererObligations,
}

Bitsets join naturally by union, while damage regions join geometrically. Structural patches need ordering and conflict rules; renderer resources may use generations. These fields belong to different domains, so collapsing them early sacrifices information.

7.9 Feedback requires an observation contract

Return to the style/layout cycle:

rust
for iteration in 0..MAX_PASSES {
    style.resolve_pending();
    layout.resolve_pending();

    let observations = geometry_observers.collect_changes();
    if observations.is_empty() {
        break;
    }

    observations.deliver(); // may write state/style
}

What happens if the loop reaches MAX_PASSES? Does the engine present the last result, freeze one side of the cycle, warn, defer remaining work to the next frame, or abort? Is the presented state equivalent to any from-scratch semantics, or merely the result of a documented bounded procedure? A reactive pipeline diagram is incomplete without this unglamorous detail.

Three consistency guarantees cover the common choices:

  1. Immediate consistency: every observation sees a fully adjusted result.
  2. Transactional consistency: writes may expose stale intermediate state internally, but observers at the transaction boundary see the adjusted result.
  3. Bounded/eventual consistency: the engine performs limited work per frame and converges over time under stated assumptions.

Bounded consistency is a legitimate choice. A UI must remain responsive under adversarial work, and unbounded stabilization can freeze the event loop. The cap is nevertheless part of the semantics: the framework must define which results may be deferred and which invariants hold at presentation.

7.10 Incrementality has a budget

Retaining more is not free. Forgetting is not primitive, and retention is not inherently virtuous.

Dependency tracking spends memory and update time to save later work. At fine granularity it also introduces allocation, pointer chasing, and larger hot structures; indexes and stable-identity maps have to be maintained even on frames that barely render. The lower pipeline pays its own rent through retained paint resources and persistent GPU surfaces. Sometimes constructing a cache key or testing equality costs more than recreating the value it protects.

We can describe a rough trade:

total cost=update work+localization metadata maintenance+retained-memory cost+cold-start cost+cleanup / eviction cost+complexity risk\begin{aligned} \text{total cost} \;=\; & \text{update work} \\ & +\; \text{localization metadata maintenance} \\ & +\; \text{retained-memory cost} \\ & +\; \text{cold-start cost} \\ & +\; \text{cleanup / eviction cost} \\ & +\; \text{complexity risk} \end{aligned}

A full view rerun may be exactly right when descriptions are cheap and the expensive retained stages have strong cutoffs. The same applies to immediate-mode passes and coarse redraws on suitably small workloads. Specialized dependency graphs and damage tracking have to earn their metadata and complexity at scale.

Useful retained information costs less to maintain than it saves in discovery or recomputation, without making correctness unmanageable. Any weaker rule would declare the framework with the most metadata the winner.

No subsystem can make that trade for the whole UI. Its choice pays off only if the next subsystem preserves it.

8. The widest seam

One scheduled signal effect, a layout cache hit, retained shaping, cached glyphs, and preserved tiles can all coexist with a broadly recomputed frame. The breadth often enters through a few lines that translate one crate’s result into another crate’s invalidation API.

Imagine this trace:

signal write
one effect
one label property mutation
mark root layout dirty
walk all layout nodes, mostly cache hits
clear scene
walk all visible nodes and rebuild primitives
glyph atlas hits
submit full-frame render pass
Every subsystem has an incremental mechanism, yet the end-to-end trace is broad.

Now imagine the opposite:

application message
rerun entire cheap view description
reconcile one changed leaf
reuse all retained widget/layout identities
reshape and rebreak one paragraph
adjust affected ancestor geometry
regenerate two paint records
replay records intersecting old/new damage
preserve undamaged pixels
The application boundary is broad, but the expensive lower stages remain localized.

View syntax alone would label the first framework “fine-grained” and the second “coarse,” exactly backwards for their expensive lower stages.

The boundary where the smallest well-typed edit becomes broad invalidation is the widest seam. There, a stage may rediscover structure already known above it, translate stable identity into ephemeral values, or simply lack vocabulary for the delta it receives.

A broad pass may be cheap enough that the widest seam does not dominate runtime. As the workload grows, however, that seam predicts where optimization pressure will appear.

9. Turning the table into an experiment

The opening table makes a better comparative experiment than a collection of slightly different lists and screenshot timings. Give each framework the same semantic edit and record the path it takes.

The application contains 100,000 logical rows and enough visible complexity to exercise structure, text, layout, paint, hit testing, and accessibility. Virtualization is a separate experimental factor, not an automatic escape hatch. A virtualized table tests the maintenance of the visible window; a fully instantiated table tests retained-tree scaling. Both are important in real-world apps.

Each framework receives the same edits:

  • Paint only: change one visible label’s foreground color without changing geometry. Can the update bypass text and layout, and how much paint, damage, and presentation work remains?
  • Text with stable geometry: replace 19 with a metrically equal string under a tabular-numeric font. Shaping and paint should change, while layout gets a genuine opportunity to cut off. “Stable” is measured rather than inferred from character count.
  • Text with reflow: use a string that changes a line break and row height. The trace now crosses intrinsic measurement, ancestor layout, later-row placement, scrolling, spatial damage, hit testing, editing state, and semantics.
  • Keyed insertion and reorder: insert near the beginning, then move a focused editable row. This exposes sequence-diff cost and every identity boundary from local state through layout and accessibility to eventual disposal.
  • Hover: move the pointer across rows without changing application data. The result shows how hit testing, pseudo-state, cursor updates, and old/new visual state interact.
  • Scroll: move first by a few pixels and then by a viewport. Compositor transforms, newly exposed content, virtualization, sticky geometry, and semantic viewport updates should become distinguishable in the trace.
  • Overlay and damage: move a translucent overlay, then give it a backdrop-reading effect. The difference probes ordering, retained paint, unchanged-overlap replay, and the renderer’s spatial dependency model.
  • Global changes: vary viewport width, scale factor, default font, locale/direction, and a root theme property. These legitimately broad dependency cones test recovery and throughput rather than locality.

For every edit, the trace vector should include:

text
ρ = (
    reactive computations run,
    descriptions rebuilt,
    nodes reconciled,
    dirty paths visited,
    style matches and cascades,
    layout nodes entered and cache misses,
    paragraphs shaped and line-broken,
    paint records regenerated and replayed,
    primitives emitted,
    damaged area,
    bytes uploaded,
    draw/compute passes,
    allocations and retired identities,
)

Why might one framework spend CPU to save retained memory while another pays complexity and cold-start cost to preserve more artifacts? The vector keeps that explanation visible. It can also show a system that is precise for paint and deliberately coarse for structure, an architectural profile that a single “incrementality score” would erase.

10. A checklist for reading the code

Each framework investigation starts with the same questions.

Retained artifacts

  • What persists between events and frames?
  • Which artifacts are owned by the application, framework, layout/text libraries, renderer, platform adapter, or GPU?
  • What is evicted, and under what budget?

Identity and lifetime

  • What names components, widget state, elements, layout nodes, paragraphs, fragments, paint records, and accessibility nodes?
  • Which identities survive equal updates, insertion, reorder, conditional removal, and moves between parents?
  • Who owns subscriptions and async work, and when are they disposed?

Change detection and localization

  • Does the system receive structured mutations, compare old/new descriptions, track reads, observe object mutations, or rely on manual invalidation?
  • Can it reach affected descendants without traversing clean siblings?
  • Are dependencies static, dynamic, tree-derived, spatial, or encoded in cache keys?

Propagation and cutoff

  • What queue or phase order runs affected work?
  • When a derived value remains equal, does propagation stop?
  • How are dynamic dependencies repaired?
  • What happens under cycles, feedback, or an exceeded work budget?

Structural update

  • Is application structure mutated in place, reconstructed and reconciled, or reissued immediately?
  • What are the keyed and unkeyed sequence semantics?
  • Does structural preservation continue into layout, paint, and accessibility?

Style and text

  • Are matching, cascade, inheritance, shaping, and line breaking separately invalidated?
  • Which properties force which obligations?
  • What forms the font/text cache keys, and what resource generations invalidate them?

Layout

  • Does invalidation propagate to ancestors, descendants, dirty roots, or exact paths?
  • Does invoking layout imply a full traversal even when cached?
  • Are measures, final layouts, or fragments retained?
  • How are intrinsic sizing and feedback handled?

Paint, rendering, and pixels

  • Are paint commands retained per node, subtree, layer, or not at all?
  • Is scene construction partial?
  • Is spatial damage tracked from old and new ink?
  • Can unchanged overlapping content be replayed without regeneration?
  • Are pixels or tiles persistent, and is final presentation still full-surface?

Other observables

  • How are hit testing, accessibility, focus, selection, caret, and IME kept consistent?
  • Are their identities and invalidations coupled to visual nodes or maintained separately?
  • Which platform effects can occur during propagation?

Evidence

  • Is the answer established by public documentation, current source, an instrumented trace, or inference?
  • Which release or commit does it describe?
  • What adversarial edit would falsify the claim?

11. Defining “incremental UI”

An incremental UI system retains some derived state across input changes and uses information about dependencies, identity, change, or demand to update that state with less work than an equivalent cold computation when the affected dependency cone is small. A sound system preserves defined observable behavior relative to its from-scratch semantics. An architectural analysis must state which layers satisfy this description, what metadata makes it possible, what localization work remains, and where deltas become coarse.

This definition does not promise sublinear work for every small source edit. “Small change, small work” is catchy and false without a semantic qualifier: one character can reflow a document, while a root font change is broad by construction and a selector can deliberately connect distant nodes. The size of the source edit is not the size of the semantic dependency cone.

Incrementality does not guarantee speed either. Bookkeeping can overwhelm the work it saves, especially when fine-grained metadata fights cache locality or retained GPU resources make cold paths expensive. Performance claims need both an architectural trace and measurements.

For our change from 19 to 20, a system should be able to tell a story:

  1. which input identity changed;
  2. which computation or node observed it;
  3. which retained entity was mutated or reconciled;
  4. which text, style, layout, semantic, and paint obligations followed;
  5. how affected work was reached;
  6. where results cut off because outputs remained equivalent;
  7. which spatial region had to be reconstructed;
  8. which resources and pixels survived;
  9. at which point the observable UI became consistent again.

“Then the whole tree is walked” is not an automatic indictment. It identifies a seam that can be measured and explained. Sometimes removing it costs more than the walk, and the simple design wins.

12. Taking the model into Rust

Rust makes these questions unusually visible. Explicit ownership and the cost of hidden shared mutation push framework authors to expose architectural choices in types and APIs. The ecosystem spans immediate-mode interfaces, reconstructed views, signal-driven retained trees, entity systems, native-renderer DOMs, and specialized DSLs. Many of them assemble the same layout, text, GPU, accessibility, and scene crates into radically different machines.

The shared substrate permits unusually direct comparisons:

  • If two frameworks use the same layout engine, why does a label update reach it differently?
  • If both use the same GPU abstraction, which one rebuilds a scene and which preserves paint records?
  • If both retain widget state, which can localize a dirty descendant?
  • If both use signals, what exactly does the scheduled effect mutate?

Part II starts below the framework API, with window loops, wgpu, Taffy, Vello, text engines, AccessKit, and Stylo. Imported components define capabilities, not integration policy. The same edits then pass through Iced, Vizia, Floem, Freya, Dioxus Native and Blitz, Xilem and Masonry, GPUI, GPUI-CE, rui, Makepad, Slint, egui, Azul, and the surrounding ecosystem.

Vello or Skia

The table still contains one hundred thousand rows, and the cell still changes from 19 to 20. At the application boundary, the edit may already be local. Part II follows it through the code to find out how long that locality survives.


Sources and further reading

The main text links sources directly, this list groups the foundational material and records the role each source plays. Software architecture changes; browser and library claims should be read at the dated snapshot indicated in the text.

Interactive systems and UI history

Browser architecture

  • Chris Harrelson and the Chromium team, RenderingNG architecture, last updated 26 July 2021. A dated architectural explanation of stage decomposition and paths that can bypass main-thread work.
  • Chromium source documentation, Blink paint README. Living source-tree documentation for paint artifacts, property trees, reuse, layerization, and raster invalidation; pin a commit for exact publication claims.
  • Mozilla, Dynamic Change Handling. Historical Gecko design documentation used for its from-scratch-equivalence and proportional-work goals, not as proof of the current implementation.
  • Mozilla, Rendering Overview. Source documentation for scene/frame distinctions and WebRender’s retained renderer structures.
  • Servo project, Layout 2013 and Layout 2020 (the Servo Layout Engines Report), 13 April 2023, and February in Servo: faster layout, pause and resume scripts, and more!, 31 March 2026. These describe different architectural generations and should not be merged into one timeless account.
  • AccessKit README, for stable semantic node IDs, initial complete trees, incremental updates, and adapter-side retention.

Incremental and reactive computation