-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
Bubble up Component dom changes up the virtual tree (+51 B) #1700
Conversation
In some situations an updating component could cause it's parent's _lastDomChild to become stale and point to a no longer mounted DOM node. This change eagerly cleans up _lastDomChild to prevent stale pointers since we don't need this pointer after its value is read in diffChildren.
@@ -2575,7 +2575,6 @@ describe('Fragment', () => { | |||
div('B') | |||
].join(''), 'updateB'); | |||
expectDomLogToBe([ | |||
// TODO: Extra appends happen here in actual. Why? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These spurious appends no longer happen!
// Eagerly cleanup _lastDomChild. We don't need to persist the value because | ||
// it is only used by `diffChildren` to determine where to resume the diff after | ||
// diffing Components and Fragments. | ||
childVNode._lastDomChild = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of trying to keep _lastDomChild
up to date, I figured it was better to just clean up the pointer after we used it and no longer need it to reduce the likelihood of having out of date pointers to DOM nodes no longer in the DOM.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking really good, I have to ask though was this issue occurring already in scenario's? Or is this something that is part of the previous PR?
Great work as always
I believe this issue was already occurring. I think it has always been broken in Preact X actually. However, I don't think I checked out 699111d (the PR before my |
Can confirm that we always had this issue in X (maybe even in 8.x?). This PR is the missing piece for our new |
src/component.js
Outdated
if (newDom != oldDom) { | ||
// Update parent component's _dom and c.base pointers | ||
while ( | ||
vnode._parent |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there cases where the _parent
pointer can be null
? If that's the case for root nodes we could try using our EMPTY_OBJ
ref for that. Maybe that could save a byte or two 🎉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh true! I'll investigate
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So many new tests 😍 Let's do this 🎉💯🚀
test/browser/components.test.js
Outdated
expect(parentDom1.base).to.equalNode(scratch.firstChild); | ||
}); | ||
|
||
it('should update parent c.base if first child becomes null', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had a look into this and it seems like sometimes the _dom
pointer of components is not set properly. When I log the vnode
in unmount()
_dom
is null whereas c.base
points to the correct DOM node.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, when looking at your comment about a missing test scenario, I realized a whole bunch of edge cases I didn't handle lol. Still working on it
removeDom(child._children); | ||
} | ||
if (child._dom) { | ||
if (typeof child.type !== 'function' && child._dom) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sventschui FYI, this PR enables the removeDom
function to now only remove the first DOM children parents under Suspense, instead of recursing all the way down the tree and removing all the nodes bottom up. Should be a bit faster now for deep trees!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome 🎉
src/component.js
Outdated
if (newDom != oldDom) { | ||
updateParentDomPointers(vnode); | ||
|
||
// // 3496 B |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: Remove all the commented code before checking in. Wanted to first give people an idea of some of the golfing techniques I tried.
P.S. Surprised this iterative approach isn't smaller than the recursive function:
Minified:
// Iterative: 3496 B
for (; null != (o = o.__p) && null != o.__c; )
for (o.__e = o.__c.base = null, r = 0; r < o.__k.length; r++)
if (null != (i = o.__k[r]) && null != i.__e) {
o.__e = o.__c.base = i.__e;
break;
}
// Recursive: 3484 B
function m(n) {
var l, u;
if (null != (n = n.__p) && null != n.__c) {
for (n.__e = n.__c.base = null, l = 0; l < n.__k.length; l++)
if (null != (u = n.__k[l]) && null != u.__e) {
n.__e = n.__c.base = u.__e;
break;
}
return m(n);
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have some guidelines about bytes saving vs. function call overhead? Asking for a friend 😂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've seen a bench (assuming it can be trusted 🤷♂) where in one situation recursion was faster than the iterative solution, so I typically go for the byte savings first on something like this, especially since this code is only called once per update.
I think it would be hard to measure the perf difference between an iterative solution and recursive in this case since it isn't called very often in a diff (as opposed to the children loop in diffChildren
which is called for every VNode, for example).
But that's just me. I don't spend as much time doing perf tests as @developit or others, so I defer to them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say let's remove the comments and get this into master since it looks awesome!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree with @JoviDeCroock 👍 💯 🚀
I'll clean it up and merge it today |
With #1700 complete, we can now rely on the sibling._dom pointer to tell us if that subtree contains a rendered DOM node and what the first dom is.
If a component changed it's DOM node and was wrapped in other Components, the parent Components
_dom
and.base
pointer could become out of date. Sincethis.base
is a public API feature on Components, I figured this need to be fixed.This might slightly decrease our memory footprint as we won't have pointers to useless DOM nodes anymore. Also fixes some cases where we would needlessly append DOM nodes to parents that are no longer mounted (see comment in fragments.test.js)
Total change: +36 B 😐Total change: +48 B 😕Total change: +51 B 😕