Skip to content

Per-render handler ids renumber the whole page, blocking P4 subtree short-circuit #640

Description

@pal-tamas

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:

  1. Wasted diff bytes and DOM writes on untouched elements, proportional to how far down the page the change was.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions