skip to content

A runtime re-describes a whole screen on each state change, yet only one text node changes — what happens in between?

level: juniorimportance: must knowfreq 72%

answer

  1. two costs, not one
  2. describing is cheap, applying is not
  3. cost follows the visited set
  4. same position, same type, compare
  5. one write reaches the host

basics

~20 s

Describing the output is cheap in-memory work. The runtime then decides which parts of that description differ from the previous update and turns only those differences into host operations, so one text write reaches the document.

solid answer

~50 s

Two separate costs hide inside one update, and confusing them is where most performance folklore comes from. First the runtime works out **what the output should be now** — a fresh tree of description objects, or a compiled set of update instructions, or simply the one binding that read the changed value. Then it works out **what to apply**: a snapshot-and-diff runtime walks the new description against the previous one at the same positions, comparing attributes and recursing into children; a compile-time targeted runtime already knows which node each dynamic value lands in; a fine-grained runtime never walks the tree because only the subscribed binding runs. All three end with the same host call — one text write. So the number worth reasoning about is *what the runtime must visit per update*, not how fast it visits: a wide subtree it re-describes every time costs more than a deep one it enters at a single branch.

go deeper

for a junior

Be able to say that the new description is compared with the previous one and only the differences are applied, so a one-label change ends as one host write.

for a middle

Explain the walk itself: same type at a position updates in place and recurses, a changed type replaces the subtree, and siblings are compared in one pass, so cost tracks the nodes visited.

for a senior

Show that you separate pass cost from host cost when diagnosing, and that your first move on a slow update is to shrink what the pass visits rather than to micro-optimise comparisons.

for a principal

Frame framework choice as a question about what a runtime must visit per update for the shapes your product actually has, rather than about benchmark throughput on a synthetic tree.

A component framework lets you write the output as a function of state: change the state, describe the screen again, and the screen catches up. Taken literally that sounds ruinous — re-describing a screen of thousands of nodes to change one label. It is not ruinous, because "describe again" and "rebuild the host" are two different things, separated by a step whose whole job is to keep the second one small. ## The vocabulary - **Host** — the thing that actually shows pixels: a document's node tree in a browser, or a native view tree. Host operations (create, update an attribute, move, remove) are the expensive end. - **Description** — what your output code produces: a tree of plain in-memory objects, or, under a compiler, a prepared set of instructions plus the dynamic values that fill them. A description is not the host; nothing is visible yet. - **Pass** — the work the runtime does between a state write and the host mutation: produce the new description for whatever it decided to visit, compare, and emit the differences. ## Two costs, not one 1. **Deciding what changed** — in-memory: running output code, allocating description nodes, comparing them. 2. **Applying what changed** — host: the create/update/move/remove calls, plus the layout and paint the host does afterwards. Minimal-change strategies exist so that cost 2 is proportional to *what the user actually sees change*. The interesting engineering question is how large cost 1 has to be to get there, because that is the cost that scales with the size of your tree rather than with the size of the change. ## What a snapshot-and-diff pass actually visits Start at the component whose state changed and produce its new description. Then compare, position by position, against the previous description: 1. **Same node type at the same position** — keep the host node, compare its attributes, write only the ones that differ, then recurse into its children. 2. **Different node type at the same position** — the old subtree is discarded and a new one is built; no attempt is made to reconcile across a type change. 3. **Children of one parent** — compared in a single pass over the sibling sequence rather than by comparing every old child against every new child. The result is that comparison work is roughly proportional to the number of description nodes the pass *visits*, and the whole game is where the pass starts and where it is allowed to stop early. It can stop early when a subtree's inputs compare equal to last time, when a branch was never marked as possibly-changed, or when a compiler proved a subtree static and lifted it out of the update path entirely. ## What each strategy must visit | Strategy | What one update visits | Where host work comes from | Shape it punishes | |---|---|---|---| | Snapshot and diff | The subtree below the changed component, minus branches it can skip | The differences the comparison found | A wide subtree re-described for a one-word change | | Compile-time targeted | Only the update instructions tied to the changed value | Direct writes to known nodes | Highly dynamic structure the compiler cannot pre-resolve | | Fine-grained subscriptions | Only the bindings that read the changed value | The attribute or text write itself | One change with a very large number of subscribers | | Whole-tree check | Every component and every binding, every pass | Bindings found to differ | A large tree with one tiny change | Note what the table does *not* say: none of these strategies rebuilds the host from the new description. The description is a plan; the host keeps its existing nodes wherever the plan matches. ## Why this framing is the useful one - **Ask what must be visited, not how fast the visit is.** Per-node speed differs by a constant factor between runtimes; visited-set size differs by orders of magnitude between two trees written in the same runtime. - **Describing is cheap relative to the host.** Allocating and comparing plain objects is ordinary in-memory work; mutating a node can force the host into layout for a whole region. - **A big description count is not a big node count.** Wrappers, fragments and conditionals inflate what a pass walks without adding anything the user can see. - **Skips are trades, not wins.** Every early-exit check costs a comparison on every update and pays back only when it actually matches. - **Measure the pass, then the host.** A slow update is either too much visiting or too much mutating, and the fix differs completely. The practical takeaway for the one-text-node case: the runtime did more in-memory work than the change deserved and exactly as much host work as the change deserved. If that update is slow, you shrink what the pass visits — you do not stop describing the output declaratively.

  • Why does the pass discard a whole subtree when the node type at a position changes, instead of trying to match inside it?
    Matching across a type change would mean a general tree comparison, which costs far more than the savings. Runtimes assume a different type at the same position means a different thing, so they build fresh. The practical consequence is that swapping a wrapper element's type at a position throws away the host nodes and any host state attached to them, such as scroll position.
  • If describing output is so cheap, why do teams still see slow updates in a snapshot-and-diff runtime?
    Because cheap per node multiplied by a large visited set is not cheap. A pass that starts high in the tree re-describes and compares everything below it that it cannot skip, on every keystroke. The fix is to shrink the visited set — start the pass lower, or give the runtime somewhere to stop early — not to make individual comparisons faster.
  • What can a runtime honestly tell you about the work it did on one update?
    Typically which components or bindings were visited and how long the pass took, aggregated per update. That is enough to see whether an update visited far more than the change warranted. It cannot tell you what the update *should* have visited, which is why the instrumentation is a starting point for restructuring rather than an answer.

saying these in an interview costs you the question

  • Thinks the whole host tree is rebuilt from the new description each update
  • Treats describing the output and mutating the host as the same cost
  • Says diffing is always faster than writing to the host directly
  • Assumes a tiny visible change implies a tiny update
  • Counts host nodes when the pass's cost follows description nodes visited