How would you decide where subtree-skip boundaries belong across a large application, and what would you ask the team to stop doing?
answer
- cost model before the tool
- structure beats gates
- measure, place, re-measure
- every gate is a stability contract
- budget visited work, not gate count
basics
~10 sStart from what the runtime must visit per update for this app's shape, then gate only where a measurement shows a large, rarely-changing region below changing state. Stop blanket-gating and prefer moving state down.
solid answer
~50 sThree principles, in order. **One: the cost model before the tool.** Ask what this runtime must visit for one update given where our state is owned; if its visited set is already close to the change, gates buy comparisons and maintenance for nothing. **Two: structure beats gates.** Moving changing state closer to what it affects, and keeping a large stable region out of the update path, removes visits instead of paying to stop them. **Three: measure, place, re-measure.** A gate is justified by an interaction whose pass is out of proportion to its change, at a hit rate high enough to earn the comparison back. What I ask the team to stop: gating by default, counting gates as a quality metric, deepening a comparison to force a hit, and shipping a gate with no before-and-after. And I name the hidden cost: every gate imposes a value-stability contract on all current and future callers.
go deeper
Take away the habit rather than the policy: never add a skip boundary because it sounds faster; add it when a measurement of a real interaction says the work below it is worth stopping.
Be able to argue both sides of a single boundary — the comparison paid every update against the pass avoided at a given hit rate — and to name the structural alternative of moving state closer to what it affects.
Show that you diagnose by interaction, order structural fixes ahead of gates, verify value stability across the boundary, and re-measure so that a gate that did nothing is deleted.
Own the organisational costs: the stability contract gates impose on all callers, the debuggability tax, staleness over time, and a budget defined as visited work per interaction rather than as optimisations applied.
Blanket memoisation is one of the most reliably negative "optimisations" a frontend codebase adopts, because its cost is small, uniform and invisible, while its benefit is concentrated in a handful of places nobody measured. Leading this well is mostly about ordering the decisions. ## Step one: establish the cost model you are actually in Before any boundary is placed, answer for the runtime in use: **what must it visit for one update, given where our state is owned?** - If the visited set is already close to the change — a design that notifies only the bindings that read a value, or a compiler that resolved which node each value lands in — there is little for a gate to prevent, and adding one is a comparison with no matching saving. - If the visited set is "the subtree below wherever state was written", gates are meaningful, and the interesting question becomes *where* the frequently-changing state is written. - If the design re-checks everything each pass, gates at component boundaries are largely beside the point; the lever is the cost and count of bindings. This step is the one most teams skip, and skipping it is how a convention ends up applied in a runtime it cannot help. ## Step two: prefer structural fixes Ranked by how much they remove rather than how much they stop: 1. **Move the frequently-changing state down**, so the pass starts closer to what the change affects and never reaches the large region at all. 2. **Keep a large stable region out of the update path** — handed to the surrounding component as content it merely places, rather than described afresh on every update. 3. **Shrink what is described**: fewer wrapper levels, fewer per-item derived values built during mapping. 4. **Only then** gate a boundary, when a pass still arrives at a big region whose inputs genuinely do not change. Structural fixes hold without a contract. A gate is a standing obligation on everyone who calls that component. ## Step three: justify each boundary with numbers A boundary earns its place when an instrumented interaction shows: - a **pass cost out of proportion** to what the user changed, and - that cost concentrated in a **region below this boundary**, and - a **hit rate** high enough that `hit rate x avoided pass > comparison on every update`, and - a **stability check**: the values crossing that boundary are the same values from update to update, not freshly built ones. Then re-measure the same interaction. A gate that does not move the number is removed, not left in place as insurance. ## What to ask the team to stop | Stop doing | Why it is harmful | |---|---| | Gating every component by default | Distributes a per-update comparison across the whole tree for savings in a few places | | Counting gates as a quality metric | Rewards the behaviour that causes the regression and makes removal look like a step backwards | | Deepening a comparison to force a hit | Walking nested contents can cost more than the pass it avoids, on every update | | Shipping a gate with no before-and-after | Leaves nobody able to tell a small win from a small regression | | Treating a gate as a correctness tool | Gates decide what is visited; using them to suppress an update you do not want hides the real cause | ## The costs a lead should say out loud - **A stability contract spreads.** Once a boundary is gated, every caller must keep passing stable values, which pushes hoisting and caching outward into code that had no performance reason to change. - **Debuggability suffers.** "Why did this not update?" is the recurring bug class, and it is answered by understanding a comparison rather than by reading the feature's logic. - **Correctness risk exists.** A gate comparing by identity will hold a stale subtree when data was changed in place rather than replaced. - **Gates go stale.** A boundary justified by one tree shape stops paying when that tree changes, and nothing fails to make that visible. ## Guardrails worth institutionalising 1. A budget expressed as **visited work per interaction** for the two or three interactions that matter most, tracked over time — not a count of optimisations applied. 2. A rule that a gate arrives with its measurement, and a periodic sweep that deletes the ones whose hit rate has collapsed. 3. A shared vocabulary for reviews: ask *what does this update visit* rather than *is this component memoised*. 4. When evaluating a runtime, judge it by what it must visit per update for the product's real shapes, and treat throughput benchmarks on synthetic trees as close to irrelevant to that question.
- What would make you decline to adopt subtree gating as a convention at all?A runtime whose visited set is already close to the change, because then each gate is a comparison with nothing behind it. Also a codebase that cannot hold the stability contract — heavy inline construction of values, many teams touching the same components — where the gates would miss constantly and cost without saving.
- How do you keep gates from silently going stale as the product grows?Tie them to a measurement that is re-run: a small set of tracked interactions with a visited-work budget, and a periodic review that removes gates whose hit rate has collapsed. Without that, a boundary justified by an old tree shape survives forever, and its removal starts to look risky to people who never saw the original number.
- A team reports that gating made no measurable difference but they want to keep it defensively. What do you say?Remove it. A gate with no measurable benefit is a comparison on every update plus a stability obligation on every caller, and its presence teaches the next engineer that gating is the default. If the concern is a future regression, the answer is the tracked interaction budget, which catches the regression wherever it comes from.
saying these in an interview costs you the question
- Treats blanket memoisation as a free default
- Reports the number of gates added as the outcome of a performance effort
- Picks a runtime on benchmark throughput rather than what it must visit
- Adds boundaries before trying to move changing state closer to its use
- Keeps a gate that never measurably helped, as insurance
- Ignores that gates force value stability on unrelated calling code