-
-
Notifications
You must be signed in to change notification settings - Fork 926
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 handling of is-elements and Fix tiny bugs of setAttr()/updateStyle() #2988
Conversation
I am a little sorry for my frequent pull requests these days.😢 I think these are bugs that almost no one cares about, so I won't hurry you to review or merge it. |
edc6ab8
to
2ae7003
Compare
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.
You still need to fix the diff algorithm (this will have negative perf impact) to compare is
values for non-custom element vnodes.
- If the
is
attribute disappears, the element needs replaced so it's no longer a custom element. - If the
is
attribute is newly added, the element needs replaced so it's now a custom element. - If the
is
attribute changes, the backing custom element needs to change to be that new type.
In all three cases, it's simpler and more predictable to just remove and re-create.
@dead-claudia I think I will close this PR for now. There is no hurry at all to fix the other bugs in this PR. |
@kfule The current behavior isn't any more "predictable". What I suggested was really just a one line fix plus some more (pretty straightforward) tests. Sorry if this wasn't clear. |
This allows the "is" attribute to be set or removed like any other attribute.
This is a leftover from MithrilJS#2811.
2ae7003
to
f944a7e
Compare
@dead-claudia The following changes related to attr/style have been added to the previous PR. Both are small changes, I think.
I have not added any tests, but I would like to add tests if needed. Also, I thought about your comment about "is" being "unpredictable".
However, I personally would be more careful about adding code to this area because of the performance impact (about a several percent drop). |
@kfule Try adding a A bit test from a field allocated inline in the object is very cheap. It's only a few bottlenecking instructions (4-6 at most) in the fast path plus an extra immediate generation + memory store (that won't bottleneck), so it should avoid significant perf deviations. You'll also have to do a |
This seems to give better performance than simply adding a `(old.attrs && old.attrs.is) == (vnode.attrs && vnode.attrs.is)` evaluation to updateNode().
@dead-claudia So, I have added code and tests to this PR to recreate the node if the |
It seems better to evaluate `old.is === vnode.is` before process that assumes node update.
Before "the call updateElement()", there was already a process that assumed an update to the same dom. Therefore, it is needed to evaluate |
The idea wasn't to replace the field access, but to not pay the cost of checking it unless it's actually there. Hope this helps. A dedicated field is fine, though. |
render/hyperscript.js
Outdated
@@ -8,8 +8,7 @@ var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\[" | |||
var selectorCache = Object.create(null) | |||
|
|||
function isEmpty(object) { | |||
for (var key in object) if (hasOwn.call(object, key)) return false | |||
return true | |||
return Object.keys(object).length === 0 |
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.
Did you confirm this to be necessary while benchmarking?
What's the tradeoff here? I didn't see anything in the commit message clarifying why it's necessary.
If it's not actually boosting anything, please revert it. It was originally made to use for ... in
for perf reasons, so I don't want that to change unless it needs to.
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.
OK, I'll revert back to the previous code.
The purpose of this change was to reduce the amount of code rather than for performance purposes.
I should have detailed the purpose as it is not directly related to this PR.
@dead-claudia |
Sorry, I found |
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.
Sorry, didn't do a detailed review the first time, my apologies.
This should be the last of my questions and requests.
} | ||
} | ||
// Some attributes may NOT be case-sensitive (e.g. data-***), | ||
// so removal should be done first to prevent accidental removal for newly setting values. |
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.
That's what the inner attrs[key] == null
condition is for in the remove loop.
Is there a test case that failed with the old code? If not, I'd like to either see one, a benchmark result, or a bundle size reduction to justify the restructuring here.
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.
That's what the inner
attrs[key] == null
condition is for in the remove loop.
Yes, that condition is the cause of the deletion issue.
An example of a test case that does not work well with the old mithril code is the flems described in the description at the top of this PR.
The performance does not seem to be affected by this code swap.
As for the bundle size, the bundle size is reduced by 1 byte gzipped due to the console.warn
move (the code old &&
is removed).
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 changed only this part and measured the bundle size.
original (current main branch)
Original size: 20,075 bytes gzipped (67,460 bytes uncompressed)
Compiled size: 8,960 bytes gzipped (24,405 bytes uncompressed)
original + swap (without console.warn move)
Original size: 20,082 bytes gzipped (67,460 bytes uncompressed)
Compiled size: 8,963 bytes gzipped (24,405 bytes uncompressed)
original + swap like this PR (with console.warn move)
Original size: 20,081 bytes gzipped (67,456 bytes uncompressed)
Compiled size: 8,959 bytes gzipped (24,402 bytes uncompressed)
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.
An example of a test case that does not work well with the old mithril code is the flems described in the description at the top of this PR.
Could you add the test case to this PR (if you don't have similar already)?
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.
Would a manual test case (like in #3002) be acceptable?
I think the dom mock does not emulate the special case handling of such cases.
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 added manual test similar to flems.
@dead-claudia |
I have benchmarked the variations related to this PR with resultsresults of `npm run perf`main (f5fc394)
main +
|
1 | 2 | 3 | 4 | 5 | max | min | mid3-average | vs main | vs "hasOwn" | |
---|---|---|---|---|---|---|---|---|---|---|
construct large vnode tree | 61175 | 60950 | 60981 | 60989 | 61067 | 61,175 | 60,950 | 61,012.33 | 100.15 | 99.53 |
rerender identical vnode | 13920420 | 13989431 | 13744925 | 14093254 | 14491479 | 14,491,479 | 13,744,925 | 14,001,035.00 | 104.79 | 97.31 |
rerender same tree | 273200 | 273269 | 271933 | 274370 | 273241 | 274,370 | 271,933 | 273,236.67 | 93.14 | 94.99 |
add large nested tree | 24918 | 24756 | 24778 | 24771 | 24885 | 24,918 | 24,756 | 24,811.33 | 93.17 | 96.42 |
mutate styles/properties | 586 | 581 | 589 | 579 | 588 | 589 | 579 | 585.00 | 98.10 | 98.04 |
repeated add/removal | 11559 | 11793 | 11676 | 11626 | 11629 | 11,793 | 11,559 | 11,643.67 | 99.82 | 98.51 |
if (hasOwn.call(attrs, "is")) vnode.is = attrs.is
in execSelector()
1 | 2 | 3 | 4 | 5 | max | min | mid3-average | vs main | vs "hasOwn" | |
---|---|---|---|---|---|---|---|---|---|---|
construct large vnode tree | 61,327 | 61,480 | 61,196 | 61,321 | 61,254 | 61,480 | 61,196 | 61,300.67 | 100.62 | 100.00 |
rerender identical vnode | 15,004,056 | 14,477,669 | 14,549,853 | 14,137,579 | 13,864,380 | 15,004,056 | 13,864,380 | 14,388,367.00 | 107.69 | 100.00 |
rerender same tree | 287,396 | 287,269 | 288,301 | 286,708 | 289,106 | 289,106 | 286,708 | 287,655.33 | 98.06 | 100.00 |
add large nested tree | 25,761 | 25,832 | 25,809 | 25,291 | 25,631 | 25,832 | 25,291 | 25,733.67 | 96.63 | 100.00 |
mutate styles/properties | 603 | 595 | 590 | 592 | 605 | 605 | 590 | 596.67 | 100.06 | 100.00 |
repeated add/removal | 12,029 | 11,794 | 11,785 | 11,880 | 11,740 | 12,029 | 11,740 | 11,819.67 | 101.33 | 100.00 |
if (attrs.is != null) vnode.is = attrs.is
in execSelector()
1 | 2 | 3 | 4 | 5 | max | min | mid3-average | vs main | vs "hasOwn" | |
---|---|---|---|---|---|---|---|---|---|---|
construct large vnode tree | 61,899 | 61,638 | 61,534 | 61,174 | 61,288 | 61,899 | 61,174 | 61,486.67 | 100.93 | 100.30 |
rerender identical vnode | 14,144,867 | 13,120,051 | 14,076,482 | 13,604,859 | 14,118,253 | 14,144,867 | 13,120,051 | 13,933,198.00 | 104.28 | 96.84 |
rerender same tree | 290,916 | 289,009 | 286,949 | 285,623 | 289,115 | 290,916 | 285,623 | 288,357.67 | 98.30 | 100.24 |
add large nested tree | 25,920 | 25,946 | 25,820 | 25,601 | 25,639 | 25,946 | 25,601 | 25,793.00 | 96.86 | 100.23 |
mutate styles/properties | 596 | 595 | 597 | 593 | 595 | 597 | 593 | 595.33 | 99.83 | 99.78 |
repeated add/removal | 12,022 | 11,889 | 12,207 | 12,035 | 11,899 | 12,207 | 11,889 | 11,985.33 | 102.75 | 101.40 |
vnode.is = attrs.is
in execSelector()
1 | 2 | 3 | 4 | 5 | max | min | mid3-average | vs main | vs "hasOwn" | |
---|---|---|---|---|---|---|---|---|---|---|
construct large vnode tree | 61,076 | 61,411 | 61,293 | 60,947 | 61,178 | 61,411 | 60,947 | 61,182.33 | 100.43 | 99.81 |
rerender identical vnode | 13,528,635 | 14,029,221 | 13,940,756 | 13,147,634 | 12,280,696 | 14,029,221 | 12,280,696 | 13,539,008.33 | 101.33 | 94.10 |
rerender same tree | 287,618 | 287,406 | 278,724 | 290,739 | 285,865 | 290,739 | 278,724 | 286,963.00 | 97.82 | 99.76 |
add large nested tree | 25,543 | 25,723 | 25,774 | 25,628 | 25,667 | 25,774 | 25,543 | 25,672.67 | 96.40 | 99.76 |
mutate styles/properties | 587 | 596 | 603 | 588 | 589 | 603 | 587 | 591.00 | 99.11 | 99.05 |
repeated add/removal | 11,891 | 11,793 | 11,899 | 11,954 | 11,970 | 11,970 | 11,793 | 11,914.67 | 102.14 | 100.80 |
return Vnode("", attrs.key, attrs, children, undefined, undefined, attrs.is)
in hyperscriptVnode()
1 | 2 | 3 | 4 | 5 | max | min | mid3-average | vs main | vs "hasOwn" | |
---|---|---|---|---|---|---|---|---|---|---|
construct large vnode tree | 63,397 | 62,940 | 62,880 | 63,038 | 62,527 | 63,397 | 62,527 | 62,952.67 | 103.34 | 102.69 |
rerender identical vnode | 14,014,474 | 13,530,013 | 14,834,992 | 14,889,124 | 13,559,975 | 14,889,124 | 13,530,013 | 14,136,480.33 | 105.81 | 98.25 |
rerender same tree | 283,727 | 285,482 | 286,921 | 286,764 | 288,033 | 288,033 | 283,727 | 286,389.00 | 97.63 | 99.56 |
add large nested tree | 26,089 | 26,113 | 25,401 | 26,112 | 26,025 | 26,113 | 25,401 | 26,075.33 | 97.92 | 101.33 |
mutate styles/properties | 580 | 597 | 593 | 596 | 603 | 603 | 580 | 595.33 | 99.83 | 99.78 |
repeated add/removal | 11,744 | 12,016 | 11,899 | 11,609 | 11,567 | 12,016 | 11,567 | 11,750.67 | 100.73 | 99.42 |
thoughts
- Compared to the
main
branch, simply adding(old.attrs && old.attrs.is) == (vnode.attrs && vnode.attrs.is)
toupdateNode()
results in a 6 to 7% drop in "rerender same tree" and "add large nested tree". The performance drop due to this PR or its variations is smaller than that (about 2 to 3% drop, which I can personally accept.). - It may be within the margin of error, but
if (attrs.is != null) vnode.is = attrs.is
seems to be a good choice. return Vnode("", attrs.key, attrs, children, undefined, undefined, attrs.is)
in hyperscriptVnode() seems to be as good asif (attrs.is != null) vnode.is = attrs.is
or possibly better.
@dead-claudia I have also measured the benchmark and it seems to me that for
Also, there is a case where |
@kfule Try |
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.
Let me know how this impacts performance. Once this is addressed, I'm more than happy to merge.
Edit: also add #2988 (comment) to the list of changes needed. |
@dead-claudia As I mentioned in my comment above, the benchmark results showed that |
To be sure, I ran the benchmark again, and it still seems to be faster with the if-condition (about 0.5%). result of
|
1 | 2 | 3 | 4 | 5 | max | min | mid3-average | vs main | vs "if (attrs.is != null)" | |
---|---|---|---|---|---|---|---|---|---|---|
construct large vnode tree | 61128 | 61463 | 61628 | 60831 | 60923 | 61,628 | 60,831 | 61,171.33 | 100.41 | 99.45 |
rerender identical vnode | 12219128 | 14976356 | 14144199 | 14020772 | 13857496 | 14,976,356 | 12,219,128 | 14,007,489.00 | 104.84 | 98.78 |
rerender same tree | 288328 | 289343 | 284116 | 283618 | 289411 | 289,411 | 283,618 | 287,262.33 | 97.92 | 99.51 |
add large nested tree | 25748 | 25843 | 25750 | 25626 | 25674 | 25,843 | 25,626 | 25,724.00 | 96.60 | 99.96 |
mutate styles/properties | 581 | 603 | 578 | 596 | 599 | 603 | 578 | 592.00 | 99.27 | 99.72 |
repeated add/removal | 12656 | 11643 | 11700 | 11616 | 11636 | 12,656 | 11,616 | 11,659.67 | 99.95 | 100.13 |
result of if (attrs.is != null) vnode.is = attrs.is
1 | 2 | 3 | 4 | 5 | max | min | mid3-average | vs main | vs "if (attrs.is != null)" | |
---|---|---|---|---|---|---|---|---|---|---|
construct large vnode tree | 61500 | 61491 | 61837 | 61100 | 61547 | 61,837 | 61,100 | 61,512.67 | 100.97 | 100.00 |
rerender identical vnode | 14527141 | 13936277 | 14911581 | 13119917 | 14079527 | 14,911,581 | 13,119,917 | 14,180,981.67 | 106.14 | 100.00 |
rerender same tree | 288368 | 286976 | 290087 | 289016 | 288641 | 290,087 | 286,976 | 288,675.00 | 98.41 | 100.00 |
add large nested tree | 25778 | 25658 | 25351 | 25808 | 25769 | 25,808 | 25,351 | 25,735.00 | 96.64 | 100.00 |
mutate styles/properties | 594 | 604 | 579 | 590 | 597 | 604 | 579 | 593.67 | 99.55 | 100.00 |
repeated add/removal | 11704 | 11886 | 11606 | 11624 | 11581 | 11,886 | 11,581 | 11,644.67 | 99.83 | 100.00 |
The if-condition case was still slightly faster, so the code is left as is for now.
As I commented above, I don't care which one you choose because the difference is not so big.
Signed-off-by: Claudia Meadows <contact@claudiameadows.dev>
Fixes a few tiny bugs in attributes and style properties updates, and improves handling of is-elements in updateNode().
Description
key === "is"
fromsetAttr()
to fix 2799 Custom elements: built-in element extensions lose theiris
attribute #2799 (comment)key === "is"
is removed from removeAttr() as well.Motivation and Context
I found a strange behavior in the process of #2985, although it rarely happens (#2985 (comment)). And there seems to be a similar trivial issue in
updateAttrs()
(flems.)This could be solved by simply swapping the order of set and removal probably without creating new bugs and perf issues.
Also, I am not very familiar with custom elements, but it seemed to me that the #2799 issue could be solved by simply removing
key === "is"
fromsetAttr()
.How Has This Been Tested?
npm run test
including test code fix for custom elementsTypes of changes
Checklist