perf(dom): List.each bulk first-emission mount fast path (#178) - #180
Open
stefvw93 wants to merge 1 commit into
Open
perf(dom): List.each bulk first-emission mount fast path (#178)#180stefvw93 wants to merge 1 commit into
stefvw93 wants to merge 1 commit into
Conversation
A List.each region's first emission has no previous state, yet it paid the full keyed-reconciliation machinery: per-item projection into immutable HashSet/HashMap, a prevIndex build, a drop-set diff, and an LIS pass over an all-new sources array. For large static lists this made first render materially slower than plain array children. reconcileList now takes a bulk mount fast path when previous state is empty (first emission, empty refill, or HY2 hydration divergence recovery): the Equal-aware duplicate guard and per-item render run as before, then all item ranges insert in one pass before the region end marker and the identity map is built in bulk. The post-mount ListState is identical to the general path, so every later reconcile behaves the same and all MR/KR/SC/ID/ER invariants hold. - Add mountListFast (private) + empty-prev branch in reconcileList. - Add list-mount-fast-path.test.ts equivalence guards (FE1/FE2/FE6). - Add perf-list-mount.browser.test.ts permanent guard (FE7): 10k static rows, structural assertions primary, plain-children baseline + generous caps. - Docs: docs/how-to/render-keyed-lists.md gains "Static lists: use plain children" guidance and a stronger by:index note. - Spec: list-mount-fast-path.specs.md; list.specs.md amendment note. No public API change. All gates green: check, test (799), test:browser (70), test:types (149/251). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jul 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #178.
What
A
List.eachregion's first emission has no previous state, yet it paid the full keyed-reconciliation machinery designed for updates: per-item projection into an immutableHashSet(dup guard) andHashMap(identity map), aprevIndexbuild, a drop-set diff, and alongestIncreasingSubsequencepass over an all--1sourcesarray. For large static lists (e.g. 10k rows), first render was materially slower than the same items as plain array children.reconcileListnow takes a bulk mount fast path when previous state is empty:prev.order.length === 0 && HashMap.isEmpty(prev.records).renderList's fresh commits, empty→refill, andhydrateList's HY2 divergence recovery (all pass an emptyprev).Equal-aware duplicate guard (projectKeys, KR1) and per-itemrenderItem(MR2, per-item scope) run exactly as the general path; then all item ranges insert in one pass before the region end marker and the identity map is built viaHashMap.fromIterable.prevIndex, the drop-set walk, and LIS (all no-ops against empty state).The returned
ListStateis identical to the general path, so every later reconcile behaves the same. This is a behavior-preserving optimization; no public API changed.Why it's equivalent
For all-new items the general path's LIS keep-set is empty, so its right-to-left anchored insert produces the same final DOM order as the fast path's single-pass insert. Renders happen before any insertion in both paths, so a render failure inserts nothing.
HashMap.fromIterableyields a lookup-equivalent identity map (keys already de-duplicated).Tests
list-mount-fast-path.test.ts(jsdom): FE6 empty→refill re-entry, FE1 bulk mount at scale, FE2 record-state parity (reorder + combined insert/remove/reorder after a fast-path mount).perf-list-mount.browser.test.ts(Chromium): FE7 permanent guard, 10k static rows, structural assertions primary (10kli+ 10k markers + order), same-run plain-children baseline + generous timing caps.list.test.tssuite already exercises the empty-prev branch (it mounts from a non-emptySubscriptionRef), andlist.hydrate.test.ts's HY2 divergence test covers the hydration-divergence entry.Validation
All four gates green locally:
vp run check,vp run test(799),vp run test:browser(70),vp run test:types(149 tests / 251 assertions).Docs
docs/how-to/render-keyed-lists.md: new "Static lists: use plain children" section and a strengthenedby: indexnote.list.specs.mdamendment + newlist-mount-fast-path.specs.md(ACs FE1–FE7).🤖 Generated with Claude Code