Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework children diffing to run in multiple phases #4162

Closed
wants to merge 15 commits into from

Conversation

andrewiggins
Copy link
Member

@andrewiggins andrewiggins commented Oct 21, 2023

Rework children diffing to run in multiple phases:

  1. Find matches for all new VNodes in the old VNode tree, and determine which VNodes moved
  2. Removed any unmatched VNodes in the old tree from the DOM
  3. Diff, move, and insert all VNodes in the new tree

TODO:

Separate matching and inserting logic since skew logic needs to run in between those
Had to move component vnode children equality check out of constructNewChildrenArray since at this point we haven't diffed the vnode and so don't know if it's gonna shortcut and reuse the existing children array, requiring us to call reorderChildren to calculate `_nextDom`
@github-actions
Copy link

github-actions bot commented Oct 21, 2023

📊 Tachometer Benchmark Results

Summary

duration

  • 02_replace1k: faster ✔ 4% - 9% (6.97ms - 15.34ms)
    preact-local vs preact-main
  • 03_update10th1k_x16: unsure 🔍 -1% - +7% (-0.42ms - +3.06ms)
    preact-local vs preact-main
  • 07_create10k: unsure 🔍 -1% - +0% (-25.63ms - +7.68ms)
    preact-local vs preact-main
  • filter_list: unsure 🔍 -3% - +1% (-0.81ms - +0.33ms)
    preact-local vs preact-main
  • hydrate1k: faster ✔ 1% - 6% (1.56ms - 9.47ms)
    preact-local vs preact-main
  • many_updates: slower ❌ 15% - 20% (3.13ms - 3.98ms)
    preact-local vs preact-main
  • text_update: unsure 🔍 -3% - +12% (-0.12ms - +0.57ms)
    preact-local vs preact-main
  • todo: slower ❌ 1% - 4% (0.54ms - 1.65ms)
    preact-local vs preact-main

usedJSHeapSize

  • 02_replace1k: slower ❌ 5% - 6% (0.17ms - 0.18ms)
    preact-local vs preact-main
  • 03_update10th1k_x16: slower ❌ 6% - 6% (0.18ms - 0.19ms)
    preact-local vs preact-main
  • 07_create10k: slower ❌ 7% - 7% (1.77ms - 1.77ms)
    preact-local vs preact-main
  • filter_list: slower ❌ 2% - 4% (0.03ms - 0.06ms)
    preact-local vs preact-main
  • hydrate1k: slower ❌ 6% - 7% (0.37ms - 0.39ms)
    preact-local vs preact-main
  • many_updates: slower ❌ 8% - 9% (0.33ms - 0.39ms)
    preact-local vs preact-main
  • text_update: unsure 🔍 -0% - +6% (-0.00ms - +0.04ms)
    preact-local vs preact-main
  • todo: slower ❌ 1% - 1% (0.01ms - 0.01ms)
    preact-local vs preact-main

Results

02_replace1k

duration

VersionAvg timevs preact-mainvs preact-localvs preact-hooks
preact-main173.13ms - 178.69ms-slower ❌
4% - 9%
6.97ms - 15.34ms
slower ❌
5% - 11%
9.13ms - 17.61ms
preact-local161.63ms - 167.88msfaster ✔
4% - 9%
6.97ms - 15.34ms
-unsure 🔍
-1% - +4%
-2.26ms - +6.69ms
preact-hooks159.34ms - 165.74msfaster ✔
5% - 10%
9.13ms - 17.61ms
unsure 🔍
-4% - +1%
-6.69ms - +2.26ms
-

usedJSHeapSize

VersionAvg timevs preact-mainvs preact-localvs preact-hooks
preact-main3.27ms - 3.27ms-faster ✔
5% - 5%
0.17ms - 0.18ms
faster ✔
6% - 6%
0.19ms - 0.20ms
preact-local3.44ms - 3.45msslower ❌
5% - 6%
0.17ms - 0.18ms
-faster ✔
0% - 1%
0.01ms - 0.03ms
preact-hooks3.46ms - 3.47msslower ❌
6% - 6%
0.19ms - 0.20ms
slower ❌
0% - 1%
0.01ms - 0.03ms
-

run-warmup-0

VersionAvg timevs preact-mainvs preact-localvs preact-hooks
preact-main62.40ms - 66.64ms-unsure 🔍
-4% - +5%
-2.49ms - +3.34ms
unsure 🔍
-5% - +3%
-3.47ms - +1.95ms
preact-local62.09ms - 66.09msunsure 🔍
-5% - +4%
-3.34ms - +2.49ms
-unsure 🔍
-6% - +2%
-3.80ms - +1.43ms
preact-hooks63.59ms - 66.96msunsure 🔍
-3% - +5%
-1.95ms - +3.47ms
unsure 🔍
-2% - +6%
-1.43ms - +3.80ms
-

run-warmup-1

VersionAvg timevs preact-mainvs preact-localvs preact-hooks
preact-main75.83ms - 79.32ms-slower ❌
1% - 8%
0.53ms - 6.02ms
unsure 🔍
-4% - +3%
-2.86ms - +2.42ms
preact-local72.18ms - 76.42msfaster ✔
1% - 8%
0.53ms - 6.02ms
-faster ✔
1% - 8%
0.59ms - 6.40ms
preact-hooks75.81ms - 79.77msunsure 🔍
-3% - +4%
-2.42ms - +2.86ms
slower ❌
1% - 9%
0.59ms - 6.40ms
-

run-warmup-2

VersionAvg timevs preact-mainvs preact-localvs preact-hooks
preact-main63.54ms - 66.40ms-slower ❌
4% - 11%
2.73ms - 6.78ms
slower ❌
3% - 10%
1.62ms - 6.20ms
preact-local58.78ms - 61.65msfaster ✔
4% - 10%
2.73ms - 6.78ms
-unsure 🔍
-5% - +2%
-3.14ms - +1.45ms
preact-hooks59.27ms - 62.85msfaster ✔
3% - 9%
1.62ms - 6.20ms
unsure 🔍
-2% - +5%
-1.45ms - +3.14ms
-

run-warmup-3

VersionAvg timevs preact-mainvs preact-localvs preact-hooks
preact-main56.98ms - 59.14ms-slower ❌
11% - 18%
5.52ms - 9.11ms
slower ❌
16% - 23%
7.89ms - 11.07ms
preact-local49.31ms - 52.18msfaster ✔
10% - 16%
5.52ms - 9.11ms
-slower ❌
1% - 8%
0.32ms - 4.01ms
preact-hooks47.42ms - 49.75msfaster ✔
14% - 19%
7.89ms - 11.07ms
faster ✔
1% - 8%
0.32ms - 4.01ms
-

run-warmup-4

VersionAvg timevs preact-mainvs preact-localvs preact-hooks
preact-main68.83ms - 72.16ms-unsure 🔍
-2% - +4%
-1.16ms - +3.05ms
unsure 🔍
-1% - +5%
-0.88ms - +3.55ms
preact-local68.26ms - 70.85msunsure 🔍
-4% - +2%
-3.05ms - +1.16ms
-unsure 🔍
-2% - +3%
-1.56ms - +2.35ms
preact-hooks67.70ms - 70.62msunsure 🔍
-5% - +1%
-3.55ms - +0.88ms
unsure 🔍
-3% - +2%
-2.35ms - +1.56ms
-

run-final

VersionAvg timevs preact-mainvs preact-localvs preact-hooks
preact-main54.57ms - 57.73ms-slower ❌
11% - 19%
5.63ms - 9.19ms
slower ❌
14% - 22%
6.75ms - 10.27ms
preact-local47.92ms - 49.56msfaster ✔
10% - 16%
5.63ms - 9.19ms
-unsure 🔍
-0% - +5%
-0.04ms - +2.23ms
preact-hooks46.86ms - 48.43msfaster ✔
12% - 18%
6.75ms - 10.27ms
unsure 🔍
-5% - +0%
-2.23ms - +0.04ms
-
03_update10th1k_x16

duration

VersionAvg timevs preact-mainvs preact-localvs preact-hooks
preact-main40.57ms - 43.10ms-unsure 🔍
-7% - +1%
-3.06ms - +0.42ms
unsure 🔍
-6% - +2%
-2.73ms - +0.79ms
preact-local41.96ms - 44.35msunsure 🔍
-1% - +7%
-0.42ms - +3.06ms
-unsure 🔍
-3% - +5%
-1.36ms - +2.06ms
preact-hooks41.58ms - 44.03msunsure 🔍
-2% - +7%
-0.79ms - +2.73ms
unsure 🔍
-5% - +3%
-2.06ms - +1.36ms
-

usedJSHeapSize

VersionAvg timevs preact-mainvs preact-localvs preact-hooks
preact-main3.22ms - 3.23ms-faster ✔
5% - 6%
0.18ms - 0.19ms
faster ✔
6% - 6%
0.20ms - 0.21ms
preact-local3.41ms - 3.42msslower ❌
6% - 6%
0.18ms - 0.19ms
-faster ✔
0% - 1%
0.01ms - 0.02ms
preact-hooks3.43ms - 3.44msslower ❌
6% - 7%
0.20ms - 0.21ms
slower ❌
0% - 1%
0.01ms - 0.02ms
-
07_create10k

duration

VersionAvg timevs preact-mainvs preact-localvs preact-hooks
preact-main1779.48ms - 1805.84ms-unsure 🔍
-0% - +1%
-7.68ms - +25.63ms
unsure 🔍
-2% - +0%
-27.91ms - +6.84ms
preact-local1773.50ms - 1793.88msunsure 🔍
-1% - +0%
-25.63ms - +7.68ms
-faster ✔
0% - 2%
4.28ms - 34.73ms
preact-hooks1791.88ms - 1814.51msunsure 🔍
-0% - +2%
-6.84ms - +27.91ms
slower ❌
0% - 2%
4.28ms - 34.73ms
-

usedJSHeapSize

VersionAvg timevs preact-mainvs preact-localvs preact-hooks
preact-main25.88ms - 25.88ms-faster ✔
6% - 6%
1.77ms - 1.77ms
faster ✔
6% - 6%
1.79ms - 1.79ms
preact-local27.64ms - 27.65msslower ❌
7% - 7%
1.77ms - 1.77ms
-unsure 🔍
-0% - -0%
-0.02ms - -0.02ms
preact-hooks27.66ms - 27.67msslower ❌
7% - 7%
1.79ms - 1.79ms
unsure 🔍
+0% - +0%
+0.02ms - +0.02ms
-
filter_list

duration

VersionAvg timevs preact-mainvs preact-localvs preact-hooks
preact-main28.44ms - 29.18ms-unsure 🔍
-1% - +3%
-0.33ms - +0.81ms
unsure 🔍
-4% - +1%
-1.10ms - +0.27ms
preact-local28.14ms - 29.01msunsure 🔍
-3% - +1%
-0.81ms - +0.33ms
-unsure 🔍
-5% - +0%
-1.38ms - +0.07ms
preact-hooks28.65ms - 29.81msunsure 🔍
-1% - +4%
-0.27ms - +1.10ms
unsure 🔍
-0% - +5%
-0.07ms - +1.38ms
-

usedJSHeapSize

VersionAvg timevs preact-mainvs preact-localvs preact-hooks
preact-main1.32ms - 1.35ms-faster ✔
2% - 4%
0.03ms - 0.06ms
faster ✔
4% - 6%
0.06ms - 0.08ms
preact-local1.38ms - 1.39msslower ❌
2% - 4%
0.03ms - 0.06ms
-faster ✔
1% - 2%
0.01ms - 0.03ms
preact-hooks1.40ms - 1.41msslower ❌
4% - 6%
0.06ms - 0.08ms
slower ❌
1% - 2%
0.01ms - 0.03ms
-
hydrate1k

duration

VersionAvg timevs preact-mainvs preact-localvs preact-hooks
preact-main166.03ms - 171.69ms-slower ❌
1% - 6%
1.56ms - 9.47ms
slower ❌
3% - 8%
4.34ms - 12.30ms
preact-local160.58ms - 166.11msfaster ✔
1% - 6%
1.56ms - 9.47ms
-unsure 🔍
-1% - +4%
-1.13ms - +6.74ms
preact-hooks157.74ms - 163.34msfaster ✔
3% - 7%
4.34ms - 12.30ms
unsure 🔍
-4% - +1%
-6.74ms - +1.13ms
-

usedJSHeapSize

VersionAvg timevs preact-mainvs preact-localvs preact-hooks
preact-main6.01ms - 6.03ms-faster ✔
6% - 6%
0.37ms - 0.39ms
faster ✔
6% - 7%
0.40ms - 0.42ms
preact-local6.40ms - 6.41msslower ❌
6% - 7%
0.37ms - 0.39ms
-faster ✔
0% - 1%
0.02ms - 0.03ms
preact-hooks6.42ms - 6.44msslower ❌
7% - 7%
0.40ms - 0.42ms
slower ❌
0% - 1%
0.02ms - 0.03ms
-
many_updates

duration

VersionAvg timevs preact-mainvs preact-localvs preact-hooks
preact-main19.96ms - 20.32ms-faster ✔
13% - 17%
3.13ms - 3.98ms
faster ✔
14% - 19%
3.24ms - 4.65ms
preact-local23.30ms - 24.07msslower ❌
15% - 20%
3.13ms - 3.98ms
-unsure 🔍
-5% - +2%
-1.18ms - +0.39ms
preact-hooks23.40ms - 24.77msslower ❌
16% - 23%
3.24ms - 4.65ms
unsure 🔍
-2% - +5%
-0.39ms - +1.18ms
-

usedJSHeapSize

VersionAvg timevs preact-mainvs preact-localvs preact-hooks
preact-main4.40ms - 4.43ms-faster ✔
7% - 8%
0.33ms - 0.39ms
faster ✔
7% - 8%
0.34ms - 0.39ms
preact-local4.75ms - 4.80msslower ❌
8% - 9%
0.33ms - 0.39ms
-unsure 🔍
-1% - +1%
-0.03ms - +0.03ms
preact-hooks4.76ms - 4.80msslower ❌
8% - 9%
0.34ms - 0.39ms
unsure 🔍
-1% - +1%
-0.03ms - +0.03ms
-
text_update

duration

VersionAvg timevs preact-mainvs preact-localvs preact-hooks
preact-main4.79ms - 5.30ms-unsure 🔍
-11% - +2%
-0.57ms - +0.12ms
faster ✔
6% - 18%
0.33ms - 1.05ms
preact-local5.03ms - 5.51msunsure 🔍
-3% - +12%
-0.12ms - +0.57ms
-faster ✔
2% - 14%
0.11ms - 0.82ms
preact-hooks5.48ms - 5.99msslower ❌
6% - 21%
0.33ms - 1.05ms
slower ❌
2% - 16%
0.11ms - 0.82ms
-

usedJSHeapSize

VersionAvg timevs preact-mainvs preact-localvs preact-hooks
preact-main0.65ms - 0.68ms-unsure 🔍
-6% - +0%
-0.04ms - +0.00ms
faster ✔
1% - 7%
0.01ms - 0.05ms
preact-local0.67ms - 0.70msunsure 🔍
-0% - +6%
-0.00ms - +0.04ms
-unsure 🔍
-4% - +2%
-0.03ms - +0.01ms
preact-hooks0.68ms - 0.71msslower ❌
1% - 7%
0.01ms - 0.05ms
unsure 🔍
-2% - +4%
-0.01ms - +0.03ms
-
todo

duration

VersionAvg timevs preact-mainvs preact-localvs preact-hooks
preact-main46.90ms - 47.62ms-faster ✔
1% - 3%
0.54ms - 1.65ms
faster ✔
4% - 6%
1.91ms - 3.12ms
preact-local47.94ms - 48.78msslower ❌
1% - 4%
0.54ms - 1.65ms
-faster ✔
2% - 4%
0.78ms - 2.06ms
preact-hooks49.29ms - 50.26msslower ❌
4% - 7%
1.91ms - 3.12ms
slower ❌
2% - 4%
0.78ms - 2.06ms
-

usedJSHeapSize

VersionAvg timevs preact-mainvs preact-localvs preact-hooks
preact-main0.78ms - 0.78ms-faster ✔
1% - 1%
0.01ms - 0.01ms
faster ✔
4% - 4%
0.03ms - 0.03ms
preact-local0.79ms - 0.79msslower ❌
1% - 1%
0.01ms - 0.01ms
-faster ✔
3% - 3%
0.02ms - 0.02ms
preact-hooks0.81ms - 0.81msslower ❌
4% - 4%
0.03ms - 0.03ms
slower ❌
3% - 3%
0.02ms - 0.02ms
-

tachometer-reporter-action v2 for Benchmarks

@github-actions
Copy link

github-actions bot commented Oct 21, 2023

Size Change: +801 B (1%)

Total Size: 58 kB

Filename Size Change
dist/preact.js 4.55 kB +136 B (2%)
dist/preact.min.js 4.58 kB +125 B (2%)
dist/preact.min.module.js 4.58 kB +127 B (2%)
dist/preact.min.umd.js 4.61 kB +126 B (2%)
dist/preact.module.js 4.58 kB +133 B (2%)
dist/preact.umd.js 4.61 kB +123 B (2%)
jsx-runtime/dist/jsxRuntime.js 369 B +9 B (2%)
jsx-runtime/dist/jsxRuntime.module.js 337 B +11 B (3%)
jsx-runtime/dist/jsxRuntime.umd.js 452 B +11 B (2%)
ℹ️ View Unchanged
Filename Size Change
compat/dist/compat.js 3.95 kB 0 B
compat/dist/compat.module.js 3.87 kB 0 B
compat/dist/compat.umd.js 4.01 kB 0 B
debug/dist/debug.js 3.5 kB 0 B
debug/dist/debug.module.js 3.5 kB 0 B
debug/dist/debug.umd.js 3.58 kB 0 B
devtools/dist/devtools.js 231 B 0 B
devtools/dist/devtools.module.js 240 B 0 B
devtools/dist/devtools.umd.js 315 B 0 B
hooks/dist/hooks.js 1.53 kB 0 B
hooks/dist/hooks.module.js 1.56 kB 0 B
hooks/dist/hooks.umd.js 1.62 kB 0 B
test-utils/dist/testUtils.js 453 B 0 B
test-utils/dist/testUtils.module.js 454 B 0 B
test-utils/dist/testUtils.umd.js 536 B 0 B

compressed-size-action

Comment on lines 56 to 59
_prevVNode: undefined,
_insert: false,
_matched: false,
_index: -1,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's definitely opportunity to consolidate these, and possibly remove obsolete fields once we further take advantage of this new approach. Will explore that more once I get Suspense tests working.

Comment on lines +2 to +4
export const EMPTY_VNODE = /** @type {import('./internal').VNode} */ (
EMPTY_OBJ
);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably unnecessary - will undo to reduce size later. Did this temporarily to make TypeScript happy in my editor.

Comment on lines 661 to 662
let shouldSearch =
remainingOldChildren > (oldVNode != null && !oldVNode._matched ? 1 : 0);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: verify terser inlines this nicely and removes the unnecessary variable for size purposes.

@coveralls
Copy link

coveralls commented Oct 21, 2023

Coverage Status

coverage: 99.291% (+0.009%) from 99.282% when pulling 6e9ba3f on two-phase-diffChildren into e24fdad on main.

WIP: suspense hydration & suspense list
Comparing children arrays in diffChildren needs to use tripe-equals strict equality to avoid having `null` and `undefined` appear equal. Children is `undefined` when mounting and oldVNode is EMPTY_OBJ. Children can be null when a component renders null.
@andrewiggins andrewiggins force-pushed the two-phase-diffChildren branch from 8820aaa to 6e9ba3f Compare October 24, 2023 13:46
andrewiggins added a commit that referenced this pull request Oct 26, 2023
…g components (#4170)

When rerendering a component we copy the component's VNode to make the oldVNode. However, copying the VNode breaks two connections in the virtual node tree: 

1) The `_parent` pointer of children of the `oldVNode` point to the original VNode, not the copied one. But in this situation, they should point to the `oldVNode` since they belong to the old tree and not the new tree we are about to rerender/reconstruct.

2) the copied VNode (`oldVNode`) doesn't exist in its parent's children array, so in `getDomSibling`, the `indexOf` search we do on a VNode's parent won't work. So calls to `getDomSibling(oldVNode)` would not return the correct result. 

Previously we discovered this issue when using null placeholders in components that setState. So the current fixes address that particular scenario by:

1) Before calling getDomSibling, we set the child's parent to `oldVNode` (called `oldParentVNode` at this point in `diffChildren`). Before that fix, parent pointer of the child VNode would point to newParentVNode, which is incorrect. In diffChildren, we set `newParentVNode._children = []` and fill it as we loop through new children, meaning child VNode would never occur in newParentVNode._children.

2) We return `_nextDom` instead of `_dom` in `getDomSibling` if it is set. This fix coincidentally worked for the test cases we had because there was only one component with no sibling components. In this situation, `getDomSibling` would return a DOM node that is about to be unmounted and so all DOM after it would be re-appended. In the situation where a component has a sibling component that also returns a Fragment, returning `_nextDom` would prevent us from seeing this new sibling component and it's dom node and and so we would incorrectly append children after it's DOM. This situation is captured in a new test case.

Now, having a deeper understanding of the issue, this PR proposes two fixes to address this problems more holistically.

1) To address the first issue, after copying the componet's VNode to `oldVNode`, we loop through the children and set their _parent pointer to `oldVNode`.

2) Instead of using `indexOf` in `getDomSibling` we will track a VNode's index in it's parent's children array as a property on the VNode, removing the need for `indexOf` in `getDomSibling`. Note: this doesn't yet fully fix the second issue ("`oldVNode` doesn't exist in the parent's children array"), but it works around that issue by relying on the fact that in this situation the VNode it was copied from will be at the same index in the parent's children array and so works for `getDomSibling`. A future PR will attempt to address this problem more directly.

So in summary, tl;dr:

1) After copying a component's VNode to rerender it, update the children to point to the copied VNode.
2) Manually track a child's index in its parent array to remove the dependence on `indexOf` in `getDomSibling`.

Also note: #4162 also adds an `_index` property to VNodes so adding that property has multiple benefits, besides this one PR
…Index...

So diffChildren can get the oldVNode out of oldChildren using _index before setting it to the VNode final position/index
@andrewiggins
Copy link
Member Author

Superseded by #4180

@andrewiggins andrewiggins deleted the two-phase-diffChildren branch October 30, 2023 22:43
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.

2 participants