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

Improve Fragment unmounting while correctly swapping nested fragments #3875

Merged
merged 2 commits into from Feb 2, 2023

Conversation

andrewiggins
Copy link
Member

@andrewiggins andrewiggins commented Feb 1, 2023

Why does #3814 fail on master?

When transitioning from the tree

<div>
  <Fragment>
    <span>0</span>
    <span>1</span>
  </Fragment>
  <input />
</div>

to the tree (note <span>1</span> was unmounted)

<div>
  <Fragment>
    <span>0</span>
  </Fragment>
  <input />
</div>

the master branch has a bug where it will re-append all the children after the Fragment. This re-appending of the <input> causes it to lose focus. The reason for this re-appending is that when exiting diffChildren for the Fragment, it's _nextDom pointer is set to <span>1</span>. Since <span>1</span> is unmounted it's parentNode is null. When diffing the input element, we hit the oldDom.parentNode !== parentDom condition in placeChild which re-appends the <input> tag and sets oldDom to null causing all siblings of <input> to re-append.

When diffing components/Fragments, the _nextDom pointer should point to where in the DOM tree the diff should continue. So when unmounting <span>1</span>, the Fragment's _nextDom should point to the <input> tag. The previous code in diffChildren removed in #3738 was intended to fix this by detecting when _nextDom pointed to a node that we were unmounting and reset it to it's sibling. However, that code (copied below) had a correctness bug prompting its removal (#3737). Let's look at this bug.

// Remove remaining oldChildren if there are any.
for (i = oldChildrenLength; i--; ) {
	if (oldChildren[i] != null) {
		if (
			typeof newParentVNode.type == 'function' &&
			oldChildren[i]._dom != null &&
			oldChildren[i]._dom == newParentVNode._nextDom
		) {
			// If the newParentVNode.__nextDom points to a dom node that is about to
			// be unmounted, then get the next sibling of that vnode and set
			// _nextDom to it
			newParentVNode._nextDom = getDomSibling(oldParentVNode, i + 1);
		}

		unmount(oldChildren[i], oldChildren[i]);
	}
}

What caused #3737?

Here is a simplified repro of the issue from #3737:

function App() {
	const [condition] = useState(true);
	return this.state.condition ? (
		// Initial render
		<>
			<div>1</div>
			<Fragment>
				<div>A</div>
				<div>B</div>
			</Fragment>
			<div>2</div>
		</>
	) : (
		// Second render: unmount <div>B and move Fragment up
		<>
			<Fragment>
				<div>A</div>
			</Fragment>
			<div>1</div>
			<div>2</div>
		</>
	);
}

The first render creates the DOM 1 A B 2 (each wrapped in divs) and when changing the condition state, it should rerender to produce A 1 2 but instead we get 1 A 2. Why?

When rerendering App we unmount <div>B</div>, which is a child of a Fragment. This unmounting triggers the call to getDomSibling in the code above. getDomSibling has a line of code in it that looks like vnode._parent._children.indexOf(vnode). This line of code doesn't work in this situation because when rerendering a component, we only make a shallow copy of the old VNode tree. So when a child of oldParentVNode (the App component in this case) tries to access it's parent through the _parent pointer (e.g. the Fragment parent of <div>A</div>), it's _parent pointer points to the new VNode tree which we are in progress of diffing and modifying. Ultimately, this tree mismatch (trying to access the old VNode tree but getting the in-progress new tree) causes us to get the wrong DOM pointer and leads to incorrect DOM ordering.

In summary, when unmounting <div>B</div>, we need getDomSibling to return <div>2</div> since that is the next DOM sibling in the old VNode tree. Instead, because our VNode pointers are mixed at this stage of diffing, getDomSibling doesn't work correctly and we get back the wrong DOM element.

Why didn't other tests catch this?

Other tests only do top-level render calls (e.g. render(<App condition={true} />) then render(<App condition={false} />)) which generate brand-new VNode trees with no shared pointers. They did not test renders originating from setState calls which go through a different code path and reuse VNode trees which share pointers across the old and new trees.

The fix

The initial fix for this is to replace getDomSibling with a call to dom.nextSibling to get the actual next DOM sibling. (I found a situation in which this doesn't work optimally. I'll open a separate PR for that.)

Final thoughts

One additional thought I have here is that walking through this has given me more confidence in our approach for v11. First, we do unmounts before insertions so we don't have to do this additional DOM pointer checking. Also, by diffing backwards, we ensure that our _next pointers are correct when we go to search what DOM element to insert an element before.

Fixes #3814, #3737

@github-actions
Copy link

github-actions bot commented Feb 1, 2023

📊 Tachometer Benchmark Results

Summary

duration

  • 02_replace1k: unsure 🔍 -9% - +9% (-12.82ms - +12.79ms)
    preact-local vs preact-master
  • 03_update10th1k_x16: unsure 🔍 -2% - +4% (-1.01ms - +1.86ms)
    preact-local vs preact-master
  • 07_create10k: unsure 🔍 -1% - +1% (-20.33ms - +25.89ms)
    preact-local vs preact-master
  • filter_list: unsure 🔍 -4% - +3% (-1.31ms - +1.01ms)
    preact-local vs preact-master
  • hydrate1k: unsure 🔍 -5% - +1% (-6.59ms - +0.91ms)
    preact-local vs preact-master
  • many_updates: unsure 🔍 -1% - +3% (-0.33ms - +0.95ms)
    preact-local vs preact-master
  • text_update: unsure 🔍 -1% - +1% (-0.04ms - +0.03ms)
    preact-local vs preact-master
  • todo: unsure 🔍 -2% - +1% (-1.21ms - +0.36ms)
    preact-local vs preact-master

usedJSHeapSize

  • 02_replace1k: unsure 🔍 -7% - +8% (-0.28ms - +0.29ms)
    preact-local vs preact-master
  • 03_update10th1k_x16: unsure 🔍 -1% - +0% (-0.03ms - +0.00ms)
    preact-local vs preact-master
  • 07_create10k: unsure 🔍 -0% - +0% (-0.00ms - +0.00ms)
    preact-local vs preact-master
  • filter_list: unsure 🔍 -0% - +0% (-0.00ms - +0.00ms)
    preact-local vs preact-master
  • hydrate1k: unsure 🔍 -0% - +0% (-0.01ms - +0.09ms)
    preact-local vs preact-master
  • many_updates: unsure 🔍 -1% - +0% (-0.04ms - +0.00ms)
    preact-local vs preact-master
  • text_update: unsure 🔍 +0% - +0% (+0.00ms - +0.00ms)
    preact-local vs preact-master
  • todo: unsure 🔍 -0% - +0% (-0.00ms - +0.00ms)
    preact-local vs preact-master

Results

02_replace1k

duration

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master128.60ms - 146.72ms-unsure 🔍
-9% - +9%
-12.79ms - +12.82ms
unsure 🔍
-12% - +4%
-17.30ms - +6.16ms
preact-local128.59ms - 146.69msunsure 🔍
-9% - +9%
-12.82ms - +12.79ms
-unsure 🔍
-12% - +4%
-17.31ms - +6.13ms
preact-hooks135.78ms - 150.68msunsure 🔍
-5% - +13%
-6.16ms - +17.30ms
unsure 🔍
-5% - +13%
-6.13ms - +17.31ms
-

usedJSHeapSize

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master3.53ms - 3.93ms-unsure 🔍
-8% - +7%
-0.29ms - +0.28ms
unsure 🔍
-5% - +9%
-0.18ms - +0.34ms
preact-local3.54ms - 3.94msunsure 🔍
-7% - +8%
-0.28ms - +0.29ms
-unsure 🔍
-5% - +9%
-0.17ms - +0.34ms
preact-hooks3.49ms - 3.81msunsure 🔍
-9% - +5%
-0.34ms - +0.18ms
unsure 🔍
-9% - +4%
-0.34ms - +0.17ms
-

run-warmup-0

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master0.93ms - 0.97ms-unsure 🔍
-2% - +4%
-0.02ms - +0.04ms
unsure 🔍
-1% - +6%
-0.01ms - +0.05ms
preact-local0.92ms - 0.96msunsure 🔍
-4% - +2%
-0.04ms - +0.02ms
-unsure 🔍
-2% - +4%
-0.02ms - +0.04ms
preact-hooks0.91ms - 0.95msunsure 🔍
-5% - +1%
-0.05ms - +0.01ms
unsure 🔍
-4% - +2%
-0.04ms - +0.02ms
-

run-warmup-1

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master0.71ms - 1.05ms-unsure 🔍
-8% - +52%
-0.05ms - +0.36ms
unsure 🔍
-0% - +60%
+0.01ms - +0.40ms
preact-local0.61ms - 0.84msunsure 🔍
-38% - +2%
-0.36ms - +0.05ms
-unsure 🔍
-15% - +28%
-0.10ms - +0.19ms
preact-hooks0.59ms - 0.77msfaster ✔
5% - 41%
0.01ms - 0.40ms
unsure 🔍
-25% - +13%
-0.19ms - +0.10ms
-

run-warmup-2

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master0.33ms - 0.45ms-unsure 🔍
-24% - +15%
-0.10ms - +0.06ms
unsure 🔍
-9% - +33%
-0.03ms - +0.11ms
preact-local0.36ms - 0.47msunsure 🔍
-16% - +27%
-0.06ms - +0.10ms
-unsure 🔍
-1% - +37%
-0.00ms - +0.13ms
preact-hooks0.32ms - 0.38msunsure 🔍
-27% - +6%
-0.11ms - +0.03ms
unsure 🔍
-29% - -1%
-0.13ms - +0.00ms
-

run-warmup-3

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master0.53ms - 1.05ms-unsure 🔍
-43% - +44%
-0.34ms - +0.34ms
unsure 🔍
-30% - +66%
-0.18ms - +0.43ms
preact-local0.56ms - 1.01msunsure 🔍
-44% - +43%
-0.34ms - +0.34ms
-unsure 🔍
-26% - +62%
-0.16ms - +0.39ms
preact-hooks0.51ms - 0.83msunsure 🔍
-50% - +19%
-0.43ms - +0.18ms
unsure 🔍
-47% - +17%
-0.39ms - +0.16ms
-

run-warmup-4

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master0.40ms - 0.43ms-unsure 🔍
-14% - +3%
-0.06ms - +0.01ms
unsure 🔍
-5% - +8%
-0.02ms - +0.03ms
preact-local0.40ms - 0.47msunsure 🔍
-3% - +16%
-0.01ms - +0.06ms
-unsure 🔍
-2% - +18%
-0.01ms - +0.07ms
preact-hooks0.39ms - 0.43msunsure 🔍
-8% - +5%
-0.03ms - +0.02ms
unsure 🔍
-16% - +1%
-0.07ms - +0.01ms
-

run-final

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master0.28ms - 0.57ms-unsure 🔍
-30% - +46%
-0.12ms - +0.18ms
unsure 🔍
-54% - +20%
-0.29ms - +0.12ms
preact-local0.35ms - 0.43msunsure 🔍
-40% - +25%
-0.18ms - +0.12ms
-unsure 🔍
-46% - +1%
-0.27ms - +0.03ms
preact-hooks0.36ms - 0.65msunsure 🔍
-33% - +73%
-0.12ms - +0.29ms
unsure 🔍
-10% - +69%
-0.03ms - +0.27ms
-
03_update10th1k_x16

duration

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master41.01ms - 42.99ms-unsure 🔍
-4% - +2%
-1.86ms - +1.01ms
unsure 🔍
-5% - +2%
-1.97ms - +0.99ms
preact-local41.39ms - 43.46msunsure 🔍
-2% - +4%
-1.01ms - +1.86ms
-unsure 🔍
-4% - +3%
-1.58ms - +1.45ms
preact-hooks41.39ms - 43.59msunsure 🔍
-2% - +5%
-0.99ms - +1.97ms
unsure 🔍
-3% - +4%
-1.45ms - +1.58ms
-

usedJSHeapSize

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master3.33ms - 3.36ms-unsure 🔍
-0% - +1%
-0.00ms - +0.03ms
unsure 🔍
-1% - +0%
-0.03ms - +0.00ms
preact-local3.32ms - 3.35msunsure 🔍
-1% - +0%
-0.03ms - +0.00ms
-faster ✔
0% - 1%
0.01ms - 0.04ms
preact-hooks3.35ms - 3.37msunsure 🔍
-0% - +1%
-0.00ms - +0.03ms
slower ❌
0% - 1%
0.01ms - 0.04ms
-
07_create10k

duration

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master1979.98ms - 2008.16ms-unsure 🔍
-1% - +1%
-25.89ms - +20.33ms
unsure 🔍
-1% - +1%
-14.00ms - +26.87ms
preact-local1978.53ms - 2015.17msunsure 🔍
-1% - +1%
-20.33ms - +25.89ms
-unsure 🔍
-1% - +2%
-14.34ms - +32.76ms
preact-hooks1972.84ms - 2002.44msunsure 🔍
-1% - +1%
-26.87ms - +14.00ms
unsure 🔍
-2% - +1%
-32.76ms - +14.34ms
-

usedJSHeapSize

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master25.36ms - 25.36ms-unsure 🔍
-0% - +0%
-0.00ms - +0.00ms
unsure 🔍
-0% - -0%
-0.02ms - -0.02ms
preact-local25.36ms - 25.36msunsure 🔍
-0% - +0%
-0.00ms - +0.00ms
-unsure 🔍
-0% - -0%
-0.02ms - -0.02ms
preact-hooks25.38ms - 25.38msunsure 🔍
+0% - +0%
+0.02ms - +0.02ms
unsure 🔍
+0% - +0%
+0.02ms - +0.02ms
-
filter_list

duration

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master34.72ms - 36.18ms-unsure 🔍
-3% - +4%
-1.01ms - +1.31ms
unsure 🔍
-2% - +4%
-0.81ms - +1.44ms
preact-local34.40ms - 36.20msunsure 🔍
-4% - +3%
-1.31ms - +1.01ms
-unsure 🔍
-3% - +4%
-1.07ms - +1.40ms
preact-hooks34.29ms - 35.99msunsure 🔍
-4% - +2%
-1.44ms - +0.81ms
unsure 🔍
-4% - +3%
-1.40ms - +1.07ms
-

usedJSHeapSize

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master1.60ms - 1.60ms-unsure 🔍
-0% - +0%
-0.00ms - +0.00ms
faster ✔
1% - 2%
0.02ms - 0.03ms
preact-local1.60ms - 1.60msunsure 🔍
-0% - +0%
-0.00ms - +0.00ms
-faster ✔
1% - 2%
0.02ms - 0.03ms
preact-hooks1.62ms - 1.63msslower ❌
1% - 2%
0.02ms - 0.03ms
slower ❌
1% - 2%
0.02ms - 0.03ms
-
hydrate1k

duration

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master118.09ms - 123.50ms-unsure 🔍
-1% - +6%
-0.91ms - +6.59ms
unsure 🔍
-3% - +4%
-3.04ms - +4.62ms
preact-local115.36ms - 120.55msunsure 🔍
-5% - +1%
-6.59ms - +0.91ms
-unsure 🔍
-5% - +1%
-5.81ms - +1.70ms
preact-hooks117.30ms - 122.71msunsure 🔍
-4% - +3%
-4.62ms - +3.04ms
unsure 🔍
-1% - +5%
-1.70ms - +5.81ms
-

usedJSHeapSize

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master18.31ms - 18.39ms-unsure 🔍
-0% - +0%
-0.09ms - +0.01ms
faster ✔
0% - 1%
0.00ms - 0.10ms
preact-local18.35ms - 18.42msunsure 🔍
-0% - +0%
-0.01ms - +0.09ms
-unsure 🔍
-0% - +0%
-0.06ms - +0.04ms
preact-hooks18.37ms - 18.44msslower ❌
0% - 1%
0.00ms - 0.10ms
unsure 🔍
-0% - +0%
-0.04ms - +0.06ms
-
many_updates

duration

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master29.72ms - 30.54ms-unsure 🔍
-3% - +1%
-0.95ms - +0.33ms
faster ✔
1% - 4%
0.24ms - 1.34ms
preact-local29.95ms - 30.93msunsure 🔍
-1% - +3%
-0.33ms - +0.95ms
-unsure 🔍
-4% - +0%
-1.09ms - +0.13ms
preact-hooks30.55ms - 31.28msslower ❌
1% - 4%
0.24ms - 1.34ms
unsure 🔍
-0% - +4%
-0.13ms - +1.09ms
-

usedJSHeapSize

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master4.62ms - 4.64ms-unsure 🔍
-0% - +1%
-0.00ms - +0.04ms
faster ✔
0% - 1%
0.02ms - 0.05ms
preact-local4.59ms - 4.63msunsure 🔍
-1% - +0%
-0.04ms - +0.00ms
-faster ✔
1% - 2%
0.04ms - 0.07ms
preact-hooks4.66ms - 4.66msslower ❌
0% - 1%
0.02ms - 0.05ms
slower ❌
1% - 2%
0.04ms - 0.07ms
-
text_update

duration

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master2.94ms - 3.00ms-unsure 🔍
-1% - +1%
-0.03ms - +0.04ms
faster ✔
4% - 6%
0.12ms - 0.19ms
preact-local2.94ms - 2.98msunsure 🔍
-1% - +1%
-0.04ms - +0.03ms
-faster ✔
4% - 6%
0.13ms - 0.20ms
preact-hooks3.10ms - 3.15msslower ❌
4% - 7%
0.12ms - 0.19ms
slower ❌
4% - 7%
0.13ms - 0.20ms
-

usedJSHeapSize

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master0.82ms - 0.82ms-unsure 🔍
-0% - -0%
-0.00ms - -0.00ms
faster ✔
2% - 2%
0.01ms - 0.01ms
preact-local0.82ms - 0.82msunsure 🔍
+0% - +0%
+0.00ms - +0.00ms
-faster ✔
2% - 2%
0.01ms - 0.01ms
preact-hooks0.83ms - 0.83msslower ❌
2% - 2%
0.01ms - 0.01ms
slower ❌
2% - 2%
0.01ms - 0.01ms
-
todo

duration

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master57.93ms - 59.21ms-unsure 🔍
-1% - +2%
-0.36ms - +1.21ms
unsure 🔍
-4% - +0%
-2.48ms - +0.29ms
preact-local57.69ms - 58.60msunsure 🔍
-2% - +1%
-1.21ms - +0.36ms
-faster ✔
0% - 5%
0.21ms - 2.83ms
preact-hooks58.43ms - 60.90msunsure 🔍
-1% - +4%
-0.29ms - +2.48ms
slower ❌
0% - 5%
0.21ms - 2.83ms
-

usedJSHeapSize

VersionAvg timevs preact-mastervs preact-localvs preact-hooks
preact-master1.10ms - 1.11ms-unsure 🔍
-0% - +0%
-0.00ms - +0.00ms
faster ✔
2% - 2%
0.02ms - 0.03ms
preact-local1.10ms - 1.11msunsure 🔍
-0% - +0%
-0.00ms - +0.00ms
-faster ✔
2% - 2%
0.02ms - 0.03ms
preact-hooks1.13ms - 1.13msslower ❌
2% - 2%
0.02ms - 0.03ms
slower ❌
2% - 2%
0.02ms - 0.03ms
-

tachometer-reporter-action v2 for Benchmarks

@coveralls
Copy link

coveralls commented Feb 1, 2023

Coverage Status

Coverage: 99.531%. Remained the same when pulling 4724c7d on fix-fragment-unmounting-swapping into e703a62 on master.

@github-actions
Copy link

github-actions bot commented Feb 1, 2023

Size Change: +142 B (0%)

Total Size: 53.1 kB

Filename Size Change
dist/preact.js 4.08 kB +23 B (0%)
dist/preact.min.js 4.1 kB +22 B (0%)
dist/preact.min.module.js 4.1 kB +21 B (0%)
dist/preact.min.umd.js 4.13 kB +25 B (0%)
dist/preact.module.js 4.1 kB +27 B (0%)
dist/preact.umd.js 4.14 kB +24 B (0%)
ℹ️ View Unchanged
Filename Size Change
compat/dist/compat.js 3.81 kB 0 B
compat/dist/compat.module.js 3.75 kB 0 B
compat/dist/compat.umd.js 3.87 kB 0 B
debug/dist/debug.js 3 kB 0 B
debug/dist/debug.module.js 3.01 kB 0 B
debug/dist/debug.umd.js 3.08 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.49 kB 0 B
hooks/dist/hooks.module.js 1.52 kB 0 B
hooks/dist/hooks.umd.js 1.57 kB 0 B
jsx-runtime/dist/jsxRuntime.js 358 B 0 B
jsx-runtime/dist/jsxRuntime.module.js 324 B 0 B
jsx-runtime/dist/jsxRuntime.umd.js 439 B 0 B
test-utils/dist/testUtils.js 442 B 0 B
test-utils/dist/testUtils.module.js 444 B 0 B
test-utils/dist/testUtils.umd.js 526 B 0 B

compressed-size-action

@@ -377,6 +383,22 @@ describe('focus', () => {
validateFocus(input, 'remove sibling before 2');
});

it('should maintain focus when removing element directly before input', () => {
Copy link
Member Author

Choose a reason for hiding this comment

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

This test cases mimics what the repro in #3814 does. I can also add that specific repro if you'd like

Copy link
Member

Choose a reason for hiding this comment

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

Fine be me the way you did it in the PR 👍

@andrewiggins andrewiggins merged commit fc5758b into master Feb 2, 2023
@andrewiggins andrewiggins deleted the fix-fragment-unmounting-swapping branch February 2, 2023 08:04
andrewiggins added a commit that referenced this pull request Feb 3, 2023
This PR is a follow up to #3875. I realized that we can't do oldChildren[i]._dom.nextSibling cuz oldChildren[i] itself could be Fragment representing a bunch of DOM nodes. We want the last dom node of oldChildren[i] and to get it's last sibling.
JoviDeCroock added a commit that referenced this pull request Jan 12, 2024
JoviDeCroock added a commit that referenced this pull request Jan 12, 2024
* backport #3871

* port test from #3884 functionality seems to work in v11

* backport #3880

* backport #3875

* backport #3862

* add todo for #3801

* backport #3868

* backport #3867

* backport #3863

* add todo for #3856

* backport #3844

* backport #3816

* backport #3888

* backport #3889
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.

Change in v10.11.1 causes inputs to lose focus on DOM changes
4 participants