How does a runtime that only checks marked subtrees differ in per-update work from one that checks every component?
answer
- where could a change be?
- marked path versus whole tree
- state owned high marks everything
- unobserved change, missed mark, stale screen
- re-evaluated bindings cost every pass
basics
~20 sA marked design visits only the region a state write flagged, so unmarked branches cost nothing, but state owned high flags everything below it. A whole-tree check visits every component and re-evaluates every binding regardless.
solid answer
~50 sThe two designs answer "where might something have changed?" in opposite ways. In a **marked** design a state write flags the component that owns it, the runtime descends only into flagged regions, and an unmarked branch is skipped wholesale, so per-update work tracks the marked region rather than the tree. In a **whole-tree check** nothing records where the change was, so the pass visits every component, re-evaluates every binding and compares each result with the one it last recorded; cost tracks tree size and binding count however small the change. Their worst cases differ: the marked design degrades when the frequently-changing state is owned high up, because the marked region becomes the whole tree, and it can leave a stale screen when a change sets no mark. The whole-tree check degrades on a large tree with a tiny change, and punishes expensive bindings, which are re-evaluated every pass.
go deeper
Know that some runtimes are told where a change happened and look only there, while others re-check everything each pass because nothing told them.
Explain both cost models in terms of what is visited, and name each one's characteristic failure: a stale screen from a change that set no mark, versus uniform work on a large tree for a tiny change.
Diagnose from symptoms: uniformly slow interactions in a big tree point one way, a screen that never updates after an out-of-band write points the other, and the fixes are structural rather than incantations.
Judge a runtime by what it must visit per update for your product's tree shape and where its changes are owned, and accept that each design's failure mode becomes your team's review burden.
Before a runtime can update as little as possible, it needs a theory about *where* a change could be. That theory is the real difference between these two designs, and it determines what a single update costs. ## Design one: marks on a path A state write does two things: it records the new value, and it flags the position that owns it as possibly-changed. The pass then starts from a root, but descends only into flagged regions. - Work per update tracks **the marked region**, not the tree. A flagged component plus everything below it that cannot be skipped. - An unmarked sibling branch is not entered at all — not re-described, not compared. - Additional early exits compose with it: a flagged path can still stop at a boundary whose inputs compare equal. Its failure modes: 1. **Marks high in the tree.** If the frequently-changing state is owned near the root, the marked region is nearly everything, and the design's advantage evaporates for exactly the updates that happen most often. 2. **A change the runtime cannot observe.** If a value is changed in a way that sets no mark — mutated in place through a reference the runtime never wrapped, or written by code outside the runtime's awareness — the screen is stale. The pass ran, visited nothing relevant, and was correct according to what it knew. 3. **Marking during a pass.** Work that writes state while the pass is running can force further passes, which is why runtimes constrain what output code may do. ## Design two: check everything With no marks, the runtime assumes any binding could have changed. Each pass walks the whole component tree, re-evaluates each binding expression, and compares the result with the value recorded last pass; differences become host writes. - Work per update tracks **tree size and binding count**, regardless of how small the change was. - It is robust against unobserved changes: a value mutated in place is noticed anyway, because the binding is re-evaluated rather than trusted. - It punishes **expensive bindings**. A binding that computes a filtered list is re-evaluated on every pass; the design's correctness comes from re-evaluating, so there is nothing to skip. - It can require **more than one pass** to settle when one binding's result feeds another, and a value that never stabilises is a genuine failure rather than a slowdown. ## Side by side | | Marked subtrees | Whole-tree check | |---|---|---| | What one update visits | The flagged region and what hangs below it | Every component, every binding | | Cost driver | Where state is owned | Size of the tree, cost of each binding | | Best shape | Deep tree, changes owned low | Small trees, or cheap bindings | | Worst shape | State owned high, changing constantly | Large tree, one tiny change | | Characteristic bug | Stale screen from a missed mark | Repeated passes, or one that never settles | | Structural fix | Move the changing state down, gate boundaries | Make bindings cheap, or shrink the checked region | ## The limit case worth knowing Fine-grained subscription pushes the marked design as far as it goes: the "mark" is the subscription recorded when a binding read the value, so a write notifies exactly the bindings that read it and no component is visited at all. Per-update work becomes proportional to the number of affected bindings. Its own cost is bookkeeping — every read registers a dependency, every write walks its subscribers — so a single write touched by a very large number of bindings is where it pays. A compiler-targeted runtime reaches a similar place from the other direction: the compiler knows statically which node each dynamic value lands in, so nothing needs marking for straight-line structure, while dynamic lists and conditionals still need runtime matching. ## What to do with this - Ask, for any runtime in front of you, **what it must visit for one update** in your app's shape. That is a design question, not a benchmark question. - If the answer is "nearly everything, because the state that changes most lives at the top", the fix is structural — move that state closer to what it affects — before it is a matter of gates or comparisons. - If the answer is "everything, always, because bindings are re-checked", the lever is making those bindings cheap and few, not reorganising ownership. - Expect the two designs to fail differently under review: one hides bugs behind missing marks, the other hides cost behind uniform work that never looks like a spike in any single place.
- Why is a whole-tree check more forgiving of a value changed in place?Because it never trusts a notification. Each pass re-evaluates the binding expression and compares the result with the value it last recorded, so a mutation that no mark would have caught still shows up as a difference. The price of that robustness is that it does the same work on every pass whether or not anything changed.
- Where does a fine-grained subscription design spend its own overhead?In bookkeeping. Every read during evaluation registers a dependency, and every write walks the subscriber list for that value, so memory and notification cost scale with the number of tracked reads rather than with the tree. It wins when a change affects few bindings, and its worst case is one very widely read value.
- A marked-subtree runtime shows a stale screen after a background task updated data. Where do you look first?At whether the write went through a channel the runtime observes. A value replaced through a tracked container sets a mark; the same data mutated in place through a plain reference, or changed by code the runtime never wrapped, sets none. The fix is to route the write so the runtime learns about it, not to force extra passes.
saying these in an interview costs you the question
- Thinks a marked design always visits less than a whole-tree check
- Assumes a whole-tree check skips components whose bindings look unchanged
- Believes a missed mark is a runtime bug rather than an unobserved change
- Says marking makes update cost independent of where state is owned
- Expects one pass to always be enough when bindings feed each other