Skip to content

perf(dom): List.each bulk first-emission mount fast path (#178) - #180

Open
stefvw93 wants to merge 1 commit into
mainfrom
feat/list-first-emission-fast-path
Open

perf(dom): List.each bulk first-emission mount fast path (#178)#180
stefvw93 wants to merge 1 commit into
mainfrom
feat/list-first-emission-fast-path

Conversation

@stefvw93

Copy link
Copy Markdown
Owner

Closes #178.

What

A List.each region's first emission has no previous state, yet it paid the full keyed-reconciliation machinery designed for updates: per-item projection into an immutable HashSet (dup guard) and HashMap (identity map), a prevIndex build, a drop-set diff, and a longestIncreasingSubsequence pass over an all--1 sources array. For large static lists (e.g. 10k rows), first render was materially slower than the same items as plain array children.

reconcileList now takes a bulk mount fast path when previous state is empty:

  • Trigger: prev.order.length === 0 && HashMap.isEmpty(prev.records).
  • Reached by renderList's fresh commits, empty→refill, and hydrateList's HY2 divergence recovery (all pass an empty prev).
  • The Effect-Equal-aware duplicate guard (projectKeys, KR1) and per-item renderItem (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 via HashMap.fromIterable.
  • Skips prevIndex, the drop-set walk, and LIS (all no-ops against empty state).

The returned ListState is 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.fromIterable yields 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 (10k li + 10k markers + order), same-run plain-children baseline + generous timing caps.
  • The existing list.test.ts suite already exercises the empty-prev branch (it mounts from a non-empty SubscriptionRef), and list.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 strengthened by: index note. list.specs.md amendment + new list-mount-fast-path.specs.md (ACs FE1–FE7).

🤖 Generated with Claude Code

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(dom): List.each first render of large static lists much slower than plain array children

1 participant