Handler ids come from a single per-render counter that resets to 0 each frame and hands out h0, h1, … in render-walk order. A component gaining or losing one handler renumbers every later handler on the page, so the diff rewrites data-rask-on-* attributes on elements that did not change.
Two costs:
- Wasted diff bytes and DOM writes on untouched elements, proportional to how far down the page the change was.
- It blocks P4 (subtree short-circuit). An "unchanged" subtree's serialized bytes are not actually stable across renders while its handler ids can shift underneath it — so there is nothing safe to cache and compare. P4 cannot land on top of the current numbering.
Prior art — a complete implementation exists, unmerged
worktree-p4-cached-subtree (d128b295, 2026-07-07, never opened as a PR) has a working version. Its design, which looks right to me and is worth reading before redoing this:
- Ids assigned per (rendering component, local handler slot) and held for the component's lifetime, so an unchanged component keeps its ids regardless of what else renders.
- Numbering anchored to
CurrentParent — the component whose Render() emits the element — not the delegate's target, so a callback passed down into a composite wrapper cannot shift the wrapper's own ids.
- Freed numbers returned to a per-root pool (
ReclaimHandlerNumbers, from the dispose pass) and reused before drawing a fresh one, so the live range is bounded by concurrent rather than cumulative handler count. That matters for a virtualized list or a growing chat log, which would otherwise climb off the interned-string table into per-handler allocations.
- Slot 0 kept as a scalar so the common single-handler component allocates nothing; multi-handler components share a geometrically-grown overflow array, reused across renders, so steady-state re-render stays allocation-neutral.
It claims first-render output is byte-identical (h0, h1, … in walk order, so no existing test changed) and that ids stay opaque to the client — no wire or API change.
What is missing before it could land
tests/Rask.Core.Tests/Live/StableHandlerIdTests.cs exists only on that branch; nothing equivalent is on main.
- No before/after benchmark was recorded, and this is render-hot-path code —
run-benchmarks evidence is required.
- It has not been rebased since 2026-07-07 and its
Component.cs conflicts with main.
The branch is retained rather than pruned for this reason. Filed so the work is not lost with the worktree; P4 remains the last open item on the DX/perf roadmap.
Handler ids come from a single per-render counter that resets to
0each frame and hands outh0, h1, …in render-walk order. A component gaining or losing one handler renumbers every later handler on the page, so the diff rewritesdata-rask-on-*attributes on elements that did not change.Two costs:
Prior art — a complete implementation exists, unmerged
worktree-p4-cached-subtree(d128b295, 2026-07-07, never opened as a PR) has a working version. Its design, which looks right to me and is worth reading before redoing this:CurrentParent— the component whoseRender()emits the element — not the delegate's target, so a callback passed down into a composite wrapper cannot shift the wrapper's own ids.ReclaimHandlerNumbers, from the dispose pass) and reused before drawing a fresh one, so the live range is bounded by concurrent rather than cumulative handler count. That matters for a virtualized list or a growing chat log, which would otherwise climb off the interned-string table into per-handler allocations.It claims first-render output is byte-identical (
h0, h1, …in walk order, so no existing test changed) and that ids stay opaque to the client — no wire or API change.What is missing before it could land
tests/Rask.Core.Tests/Live/StableHandlerIdTests.csexists only on that branch; nothing equivalent is on main.run-benchmarksevidence is required.Component.csconflicts with main.The branch is retained rather than pruned for this reason. Filed so the work is not lost with the worktree; P4 remains the last open item on the DX/perf roadmap.