When is skipping a subtree because its inputs compare equal actually cheaper than re-running it?
answer
- the check is not free
- paid every update, saves sometimes
- hit rate times subtree pass cost
- fresh inputs never compare equal
- big and stable is the win
basics
~20 sA skip pays a comparison on every update and saves the subtree's pass only when the inputs match. It wins above a large, rarely-changing subtree, and loses on cheap ones or when a caller rebuilds the inputs each update.
solid answer
~50 sThe bailout is a trade, so price both sides. The **cost** is a comparison of the subtree's inputs, paid on every single update that reaches that position — usually shallow, so proportional to the number of inputs. The **saving** is the pass that would have run below it: re-describing the subtree and comparing it against the previous description, proportional to the subtree's size. So the skip pays off roughly when `hit rate x subtree pass cost > comparison cost`. That means it is worth it above a big, rarely-changing subtree, and a pure tax above a shallow one, or anywhere a caller hands down a freshly built value each update so the check can never match. Two other facts matter: a skipped subtree still updates when its *own* state changes, and in runtimes whose visited set is already tiny — fine-grained tracking or compile-time targeting — there is usually no comparison to add, because nothing below was going to be visited anyway.
go deeper
Remember that a runtime can skip a subtree when the values passed into it are unchanged, and that the skip is decided before any of that subtree's output code runs.
Be ready to price both sides out loud: a shallow comparison on every update against a saved pass proportional to the subtree, and explain why a newly built input makes the check miss forever.
Demonstrate that you place gates from measurements on real interactions, verify the stability of the values crossing that boundary, and treat a low hit rate as a reason to delete the gate.
Own the tradeoff that every gate imposes a stability contract on all present and future callers, and decide whether the codebase can honour that contract before making gating a convention.
An equal-input bailout is the runtime asking one question before it descends: *have this subtree's inputs changed since last time?* If the answer is no, the subtree is not re-described and not compared; the previous result stands. It is the cheapest way to make an update visit less, and the easiest thing in a UI codebase to apply where it does nothing. ## What the check is, precisely - It runs **at a boundary** — a component position that the runtime has been told to gate. Some runtimes gate every component boundary by default; others gate only where you mark it. - It compares the **inputs the parent passes in**, typically with a shallow comparison: input by input, by identity rather than by walking nested contents. - It happens **before** the subtree's output code runs. Nothing below is described, so nothing below is compared. - It is a decision about **this pass only**. The next state write re-asks the question. What the check is *not*: a promise that the subtree will not update. A component skipped because its parent's inputs were unchanged still re-runs when its own state changes, or when something it reads directly changes. The gate only removes the *parent-driven* visit. ## Pricing both sides | | Paid | Proportional to | Frequency | |---|---|---|---| | Cost of the check | Every update that reaches the boundary | Number of inputs compared | 100% of updates | | Saving from the skip | Only when the check matches | Size of the subtree's pass | The check's hit rate | So the honest rule of thumb is: 1. Estimate the **subtree pass cost** — how many description nodes would be produced and compared below this boundary. 2. Estimate the **hit rate** — across the updates this app really performs, how often are these inputs genuinely unchanged? 3. Compare `hit rate x pass cost` with the comparison cost you now pay every time. Three consequences fall out of that arithmetic: - **Big and stable wins.** A dense subtree whose inputs change on one update in twenty is the textbook case; the comparison is noise against the pass it removes. - **Small or churning loses.** Above a handful of nodes, or above a boundary whose inputs change on nearly every update, you have added a comparison to every update and removed almost nothing. - **A never-matching check is pure cost.** If a caller constructs a fresh value — a new object, array or inline function — as an input on each update, a shallow comparison finds a different input every time. The gate runs, misses, and the subtree runs anyway. This is the most common way a bailout ends up costing more than it saves. ## Where the skip becomes reachable at all The gate can only help if the pass would otherwise have arrived. That has structural consequences: - If the frequently-changing state sits **above** a large static region, every update walks toward it, and a gate is the tool for stopping. If that state can instead live **below** the static region, the pass never goes there and no comparison is needed. - A subtree handed to a component as content it merely places, rather than described afresh by that component each update, tends to arrive with a stable identity, which is what makes an equality check able to match at all. - Under a reactivity model that tracks reads per binding, or a compiler that resolved which node each value lands in, the visited set is already close to the change itself. Adding gates there buys little and still costs the comparison. ## How this goes wrong in practice - **Blanket application.** Gating every boundary distributes a small tax across the whole tree in exchange for savings concentrated in a few places. - **Deep comparison to force a hit.** Swapping a shallow check for one that walks nested contents can cost more than the pass it avoids, especially on large inputs. - **Correctness drift.** A gate that compares by identity will hold a stale subtree if something was changed in place rather than replaced, which turns a performance tweak into a "why didn't the screen update" bug. - **No measurement.** Without a before-and-after on a real interaction, you cannot tell a 3% win from a 2% regression, and both are common. The discipline that survives contact with a codebase is simple: measure the interaction, find the update whose visited set is out of proportion to the change, place a gate at the one boundary that makes the pass stop short of a large stable region, verify that the inputs crossing that boundary are actually stable, and re-measure. Everything else is a comparison you pay for forever.
- Why does a freshly built object or inline function passed as an input defeat the bailout?Because the gate's comparison is shallow: it asks whether each input is the same value as last time, not whether its contents look alike. A value constructed during the parent's update is a different value every time, so the check misses on every update. You either keep that value stable across updates or remove the gate, because as written it only adds cost.
- A subtree is skipped because its inputs are unchanged, but its own state changes a moment later. What happens?It updates. The gate only suppresses the parent-driven visit; a write to the subtree's own state schedules it directly, and the pass resumes from there. Treating a gate as a freeze on a subtree is a misreading that leads people to blame bailouts for updates they can actually see happening.
- How would you decide whether an existing bailout is earning its keep?Instrument the interaction and look at two numbers: how often the check matches, and how much pass work it removes when it does. A low hit rate means the comparison is a tax; a high hit rate over a trivially small subtree means the saving is noise. Remove it in both cases and keep the ones where a large stable region stops the pass.
Phoning ahead to ask whether anything changed is worth it only when the trip it might save is long and usually unnecessary. If the trip is two minutes, or things always changed, the call is pure overhead.
saying these in an interview costs you the question
- Believes a skip marker makes a component faster in every case
- Thinks the input comparison is only paid when something changed
- Wraps every boundary in a gate and calls it an optimisation
- Assumes a skipped subtree cannot update from its own state
- Reaches for a deep comparison of inputs to force the check to match
- Adds bailouts without measuring the interaction before and after