Compute minimal child moves with a longest increasing subsequence#5174
Open
JoviDeCroock wants to merge 4 commits into
Open
Compute minimal child moves with a longest increasing subsequence#5174JoviDeCroock wants to merge 4 commits into
JoviDeCroock wants to merge 4 commits into
Conversation
Replace the skew heuristic's insertion marking with a patience-sort pass over the matched old indices. Children on the longest increasing subsequence stay in place; everything else is marked INSERT_VNODE. The pass only runs when a match lands more than one position away from its skewed index - pure shifts provably keep matched old indices in increasing order and skip it entirely. Reorders like moving a prefix to the end (997 insertBefore calls -> 3 on a 1000 row list), middle swaps, and intermingled moves now perform the minimal number of DOM operations. Costs +85 B brotli. Updated DOM op expectations are equal or better in every changed test.
📊 Tachometer Benchmark ResultsSummaryduration
usedJSHeapSize
Resultscreate10kduration
usedJSHeapSize
filter-listduration
usedJSHeapSize
hydrate1kduration
usedJSHeapSize
many-updatesduration
usedJSHeapSize
replace1kduration
usedJSHeapSize
run-warmup-0
run-warmup-1
run-warmup-2
run-warmup-3
run-warmup-4
run-final
text-updateduration
usedJSHeapSize
tododuration
usedJSHeapSize
update10th1kduration
usedJSHeapSize
|
|
Size Change: +106 B (+0.67%) Total Size: 15.9 kB 📦 View Changed
ℹ️ View Unchanged
|
Replaces the linear top-down scan of the tails stack. The scan was smaller (~14 B) and O(1) amortized for mostly in-order children, but degraded to O(n^2) for large half-shuffled lists; binary search keeps the whole pass at a guaranteed O(n log n).
Truthiness check instead of the NULL comparison in the forward pass, and reuse i (left at newChildrenLength by the forward loop) for the backward walk. Same algorithm and complexity; 9 B brotli smaller. Measured but rejected: replacing the skew if/else chain with offset arithmetic and hoisting repeated member reads into locals both minify smaller yet compress larger, since they break up byte sequences brotli matches elsewhere in the bundle.
Mirrors the edge-case coverage added to the v10.x displacement heuristic (#5172), where the minimal-move pass produces identical operation logs for every case: - a far swap moves only the two swapped children - displacing more than half the list moves the shorter suffix - displacement combined with an appended or removed child - three consecutive displacements to catch state accumulation issues - correctness of raw text siblings around displaced keyed children
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.
Note
This PR was authored with Claude Fable 5
What
Replaces the skew heuristic's
INSERT_VNODEmarking inconstructNewChildrenArraywith a longest-increasing-subsequence (patience sort) pass over the matched old indices — the approach Inferno and Vue 3 use. Children on the subsequence stay in place; everything else is marked for insertion. This yields the minimal number of DOM moves for every reorder pattern: prefix/suffix displacement, middle swaps, intermingled moves, reversals.The skew bookkeeping itself stays — it still centers
findMatchingIndex's search. Only the move decision changes.How
MATCHEDflag on the new vnode as an eligibility marker for the LIS pass (it's already cleared unconditionally indiffChildren's placement loop, which is exactly what maintains the existing "oldChildren enter unmatched" invariant). Unsuspending children keep taking the mount path and are excluded automatically.moved: it only runs when some match lands more than one position from its skewed index. For pure shift patterns (prepends, removals, insertions) the matched old indices are provably already increasing, so the gate is exact and the common path pays a single boolean.tailsstack, keeping the pass at O(n log n) even for pathological reorders. The backward pass keeps the last chain (lisLengths[i] == remaining length) and flags the rest.Numbers
Size: +99 B brotli on
preact.mjs.Tachometer (Chrome headless, 3-way vs 10.29.7-era baseline and the #5172 heuristic, using the new reorder1k benchmark — move first 3 of 1,000 keyed rows to the end):
reorder1k goes from 997
insertBeforecalls to 3. The non-reorder benches confirm themovedgate keeps in-place updates free. Relative to #5172 (the v10.x prefix-move heuristic) this generalizes the same win to every reorder shape for ~55 B more; the two approaches supersede each other, so #5172 stays v10.x-only and this is the v11 form.Notes
skewis reused as the backward-pass counter purely for bytes.