Skip to content

feat: use longest increasing subsequence discovery to minimize DOM node moves - #5184

Open
jviide wants to merge 3 commits into
preactjs:mainfrom
jviide:manneken-lis
Open

feat: use longest increasing subsequence discovery to minimize DOM node moves#5184
jviide wants to merge 3 commits into
preactjs:mainfrom
jviide:manneken-lis

Conversation

@jviide

@jviide jviide commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

This pull request is a take on the longest increasing subsequence (LIS) patience sort algorithm for determining which children are already in the correct relative order. See #5174 for an alternative implementation. The aim here is to also optimize implementation size by weaving the implementation into the constructNewChildrenArray main loop.

Benchmarks against main and #5174 seems to show both LIS implementations performing similarly:

Benchmark Main This PR #5174
reorder1k 50.0 ms 16.3 ms 16.6 ms
update10th1k 16.3 ms 16.4 ms 16.4 ms
create10k 441.9 ms 443.8 ms 443.2 ms
many-updates 17.0 ms 17.4 ms 17.4 ms

There are a couple of implementation trade-offs:

  • It creates the piles array for every diffChildren() call. On the other hand avoids a separate per-index LIS tracking array by reusing VNode._depth.
  • It always runs the LIS algorithm, but narrows the binary-search range and skips the search entirely in the best case.

This change also frees the skew mechanism from the reponsibility of preserving relative node ordering (as LIS now always takes care of that), so there should be more leeway to experiment with the skew heuristics for node matching.

Total byte increase for preact.mjs.br vs. the current main version: +53 B

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

📊 Tachometer Benchmark Results

Summary

duration

  • create10k: slower ❌ 0% - 1% (2.03ms - 9.44ms)
    preact-local vs preact-main
  • filter-list: unsure 🔍 +0% - +0% (+0.00ms - +0.06ms)
    preact-local vs preact-main
  • hydrate1k: unsure 🔍 -2% - +1% (-0.88ms - +0.60ms)
    preact-local vs preact-main
  • many-updates: slower ❌ 0% - 5% (0.03ms - 0.88ms)
    preact-local vs preact-main
  • replace1k: unsure 🔍 -0% - +2% (-0.05ms - +0.94ms)
    preact-local vs preact-main
  • text-update: unsure 🔍 -2% - +6% (-0.03ms - +0.11ms)
    preact-local vs preact-main
  • todo: slower ❌ 1% - 2% (0.18ms - 0.60ms)
    preact-local vs preact-main
  • update10th1k: unsure 🔍 -5% - +1% (-1.41ms - +0.23ms)
    preact-local vs preact-main

usedJSHeapSize

  • create10k: unsure 🔍 -0% - +0% (-0.02ms - +0.03ms)
    preact-local vs preact-main
  • filter-list: unsure 🔍 +0% - +0% (+0.00ms - +0.01ms)
    preact-local vs preact-main
  • hydrate1k: unsure 🔍 -2% - +5% (-0.09ms - +0.25ms)
    preact-local vs preact-main
  • many-updates: unsure 🔍 +0% - +0% (+0.01ms - +0.01ms)
    preact-local vs preact-main
  • replace1k: slower ❌ 0% - 1% (0.00ms - 0.02ms)
    preact-local vs preact-main
  • text-update: slower ❌ 0% - 7% (0.01ms - 0.07ms)
    preact-local vs preact-main
  • todo: slower ❌ 1% - 1% (0.01ms - 0.01ms)
    preact-local vs preact-main
  • update10th1k: slower ❌ 0% - 1% (0.00ms - 0.02ms)
    preact-local vs preact-main

Results

create10k

duration

VersionAvg timevs preact-localvs preact-main
preact-local692.68ms - 697.85ms-slower ❌
0% - 1%
2.03ms - 9.44ms
preact-main686.88ms - 692.19msfaster ✔
0% - 1%
2.03ms - 9.44ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local19.23ms - 19.27ms-unsure 🔍
-0% - +0%
-0.02ms - +0.03ms
preact-main19.23ms - 19.26msunsure 🔍
-0% - +0%
-0.03ms - +0.02ms
-
filter-list

duration

VersionAvg timevs preact-localvs preact-main
preact-local16.56ms - 16.60ms-unsure 🔍
+0% - +0%
+0.00ms - +0.06ms
preact-main16.52ms - 16.57msunsure 🔍
-0% - -0%
-0.06ms - -0.00ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local1.57ms - 1.57ms-unsure 🔍
+0% - +0%
+0.00ms - +0.01ms
preact-main1.56ms - 1.56msunsure 🔍
-0% - -0%
-0.01ms - -0.00ms
-
hydrate1k

duration

VersionAvg timevs preact-localvs preact-main
preact-local56.90ms - 57.82ms-unsure 🔍
-2% - +1%
-0.88ms - +0.60ms
preact-main56.92ms - 58.08msunsure 🔍
-1% - +2%
-0.60ms - +0.88ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local5.05ms - 5.31ms-unsure 🔍
-2% - +5%
-0.09ms - +0.25ms
preact-main5.00ms - 5.21msunsure 🔍
-5% - +2%
-0.25ms - +0.09ms
-
many-updates
  • Browser: chrome-headless
  • Sample size: 100
  • Built by: CI #5748
  • Commit: f9a30c0

duration

VersionAvg timevs preact-localvs preact-main
preact-local16.63ms - 17.46ms-slower ❌
0% - 5%
0.03ms - 0.88ms
preact-main16.49ms - 16.68msfaster ✔
0% - 5%
0.03ms - 0.88ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local3.76ms - 3.76ms-unsure 🔍
+0% - +0%
+0.01ms - +0.01ms
preact-main3.75ms - 3.75msunsure 🔍
-0% - -0%
-0.01ms - -0.01ms
-
replace1k
  • Browser: chrome-headless
  • Sample size: 110
  • Built by: CI #5748
  • Commit: f9a30c0

duration

VersionAvg timevs preact-localvs preact-main
preact-local41.90ms - 42.72ms-unsure 🔍
-0% - +2%
-0.05ms - +0.94ms
preact-main41.59ms - 42.15msunsure 🔍
-2% - +0%
-0.94ms - +0.05ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local3.04ms - 3.05ms-slower ❌
0% - 1%
0.00ms - 0.02ms
preact-main3.03ms - 3.04msfaster ✔
0% - 1%
0.00ms - 0.02ms
-

run-warmup-0

VersionAvg timevs preact-localvs preact-main
preact-local20.24ms - 20.77ms-unsure 🔍
-1% - +3%
-0.20ms - +0.58ms
preact-main20.03ms - 20.61msunsure 🔍
-3% - +1%
-0.58ms - +0.20ms
-

run-warmup-1

VersionAvg timevs preact-localvs preact-main
preact-local22.70ms - 23.37ms-unsure 🔍
-3% - +1%
-0.75ms - +0.33ms
preact-main22.83ms - 23.67msunsure 🔍
-1% - +3%
-0.33ms - +0.75ms
-

run-warmup-2

VersionAvg timevs preact-localvs preact-main
preact-local23.17ms - 24.10ms-slower ❌
8% - 14%
1.70ms - 2.89ms
preact-main20.97ms - 21.70msfaster ✔
7% - 12%
1.70ms - 2.89ms
-

run-warmup-3

VersionAvg timevs preact-localvs preact-main
preact-local19.25ms - 19.50ms-slower ❌
1% - 2%
0.12ms - 0.42ms
preact-main19.02ms - 19.19msfaster ✔
1% - 2%
0.12ms - 0.42ms
-

run-warmup-4

VersionAvg timevs preact-localvs preact-main
preact-local21.10ms - 21.75ms-slower ❌
3% - 8%
0.59ms - 1.56ms
preact-main19.99ms - 20.70msfaster ✔
3% - 7%
0.59ms - 1.56ms
-

run-final

VersionAvg timevs preact-localvs preact-main
preact-local15.19ms - 15.47ms-slower ❌
0% - 3%
0.01ms - 0.38ms
preact-main15.02ms - 15.26msfaster ✔
0% - 2%
0.01ms - 0.38ms
-
text-update
  • Browser: chrome-headless
  • Sample size: 220
  • Built by: CI #5748
  • Commit: f9a30c0

duration

VersionAvg timevs preact-localvs preact-main
preact-local1.90ms - 2.01ms-unsure 🔍
-2% - +6%
-0.03ms - +0.11ms
preact-main1.87ms - 1.96msunsure 🔍
-6% - +1%
-0.11ms - +0.03ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local1.06ms - 1.11ms-slower ❌
0% - 7%
0.01ms - 0.07ms
preact-main1.02ms - 1.07msfaster ✔
1% - 7%
0.01ms - 0.07ms
-
todo

duration

VersionAvg timevs preact-localvs preact-main
preact-local32.52ms - 32.75ms-slower ❌
1% - 2%
0.18ms - 0.60ms
preact-main32.07ms - 32.42msfaster ✔
1% - 2%
0.18ms - 0.60ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local1.29ms - 1.29ms-slower ❌
1% - 1%
0.01ms - 0.01ms
preact-main1.27ms - 1.27msfaster ✔
1% - 1%
0.01ms - 0.01ms
-
update10th1k
  • Browser: chrome-headless
  • Sample size: 130
  • Built by: CI #5748
  • Commit: f9a30c0

duration

VersionAvg timevs preact-localvs preact-main
preact-local29.56ms - 30.60ms-unsure 🔍
-5% - +1%
-1.41ms - +0.23ms
preact-main30.04ms - 31.30msunsure 🔍
-1% - +5%
-0.23ms - +1.41ms
-

usedJSHeapSize

VersionAvg timevs preact-localvs preact-main
preact-local2.99ms - 3.00ms-slower ❌
0% - 1%
0.00ms - 0.02ms
preact-main2.98ms - 2.99msfaster ✔
0% - 1%
0.00ms - 0.02ms
-

tachometer-reporter-action v2 for CI

@jviide
jviide force-pushed the manneken-lis branch 2 times, most recently from c85b0f5 to c86f509 Compare July 29, 2026 23:35
@coveralls

coveralls commented Jul 29, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 98.534%. remained the same — jviide:manneken-lis into preactjs:main

@jviide
jviide force-pushed the manneken-lis branch 2 times, most recently from d63b420 to 84c0d9a Compare July 30, 2026 18:51
jviide and others added 3 commits July 30, 2026 22:08
Mirrors the edge-case coverage added to the v10.x displacement heuristic
(preactjs#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
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.

3 participants