Skip unchanged {{#each}} item subtrees during updates - #21544
Skip unchanged {{#each}} item subtrees during updates#21544NullVoxPopuli-ai-agent wants to merge 2 commits into
Conversation
The UpdatingVM walks every updating opcode of every list item on every render: cache groups (JumpIfNotModifiedOpcode) exist only at component boundaries, so a list of plain template rows revalidates every binding even when nothing in a row changed. Collect each item's consumed tags in a tracking frame (via a new frame-finalizer hook on UpdatingVMFrame) and skip the item's entire subtree while that combined tag validates. Trivial items opt out: for a text node or two, validating a combined tag costs as much as updating, so collection would be pure overhead. An item is trivial when it has <= 2 opcodes and no nested block -- a nested block child means an arbitrarily large subtree hides behind a small top-level count. dbmon-style workloads (fat rows, sparse changes): ~1.6x fps at 8x CPU throttle, ~6x (rAF-capped) at 4x. Dense-change / tiny-item workloads and the krausest bench: neutral. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Isolated same-batch measurement (today's
🤖 Generated with Claude Code |
| } | ||
| } | ||
|
|
||
| export class ListItemOpcode extends TryOpcode { |
There was a problem hiding this comment.
all these changes are for the list op
There was a problem hiding this comment.
Correct — the entire mechanism lives in the list opcodes (ListItemOpcode/ListBlockOpcode). That's deliberate: the VM's existing skip machinery (cache groups / JumpIfNotModified) exists only at component boundaries, and list items are exactly where large clean-subtree walks concentrate without any component boundary to catch them — dbmon's rows being the canonical case. Non-list opcodes keep their behavior untouched, which is also what keeps the blast radius reviewable.
|
The two suites agreeing-by-disagreeing is the mechanism working as designed: the skip's win scales with (walk size) × (clean-item fraction) × (update frequency).
🤖 Generated with Claude Code |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>


Re-cut of #21512 (closed during the #21520 spike consolidation) onto current
main, now with the full-suite verification that was outstanding: 9443 tests, 0 failures locally.During revalidation, each
{{#each}}item collects the tags its subtree consumed (via a tracking-frame finalizer on the item's try-frame); on later passes, an item whose collected tag validates clean is skipped entirely instead of walking every opcode of its subtree. A triviality gate (≤2 opcodes and no nested block) keeps the bookkeeping off items too small to profit. Measured standalone at ~1.9x on an 8x-throttled dbmon (walk-heavy, many clean items per frame); no observable behavior change — the same subtrees re-render for the same reasons, cheaper.This was the campaign's first lever and its most reviewed artifact; it needs no RFC, no flag, and composes with (but does not require) the scheduler work in #21493/#21520.
🤖 Generated with Claude Code