fix(animations): make sure query works with shadow dom elements#46488
fix(animations): make sure query works with shadow dom elements#46488dario-piotrowicz wants to merge 4 commits intoangular:mainfrom
Conversation
JoostK
left a comment
There was a problem hiding this comment.
I left some suggestions to reduce memory pressure.
There was a problem hiding this comment.
This generates quite a bit of garbage (intermediate arrays) which can easily be avoided if we just iterate of element.children and then element.shadowRoot.children (have two loops instead of one).
There was a problem hiding this comment.
good point, code wise it will not be as nice but you're totally right
There was a problem hiding this comment.
I did that via a single look over the two arrays, may look a bit ugly, please let me know what you think
(the alternative would be to either have duplicated code or define a local function to extract the logic, please let me know if those alternatives seem better to you)
There was a problem hiding this comment.
This function can likely become a top-level function? It doesn't capture anything from the _query arrow block.
There was a problem hiding this comment.
well, it sets the starting element to true, I would not want that to be done by the caller (as it would also change the API), I could have the isStartElement flag defaulting to true, but I find it more clear/clean this way, the inner function is encapsulated in the outer which exposes the correct interface without the caller needing, or even being able to provide the values that it should not care about
do you disagree?
There was a problem hiding this comment.
Oh I was suggesting to keep _query as is, but to move queryFn out to a top-level function (non-exported). That way the _query function still dictates the initial call into queryFn.
There was a problem hiding this comment.
ah ok, yeah that makes sense 🙂👍
fix the query function not taking into consideration element inside components using the shadowDom view encapsulation
address JoostK comments
|
@JoostK thanks a lot for the awesome suggestions! 😃 I replied to all of them (and applied most), please have another look 🙂 About the accumulator, I am not sure if you had in mind something different but I am returning it and re-assigning it (this should not add overhead, let me know if this is not ok) |
06658f7 to
7186b9f
Compare
There was a problem hiding this comment.
I only realize now that this completely replaces querySelectorAll with a "manual" DOM walk. We'd really have to see how this affects performance, as I can imagine that querySelectorAll is able to leverage optimizations that we won't be able to leverage now.
If performance does take a hit, an alternative might be to walk the DOM using a TreeWalker with NodeFilter.SHOW_ELEMENT filter. I don't know how that interacts with shadow roots, perhaps the NodeFilter.SHOW_DOCUMENT_FRAGMENT filter also affects this (would need some experimentation, I don't know how the TreeWalker API behaves exactly).
There was a problem hiding this comment.
Oh I was suggesting to keep _query as is, but to move queryFn out to a top-level function (non-exported). That way the _query function still dictates the initial call into queryFn.
| const childEls = Array.from(element.children); | ||
| const shadowChildEls = element.shadowRoot ? Array.from(element.shadowRoot.children) : []; |
There was a problem hiding this comment.
HTMLCollection is iterable (hence you can pass it into Array.from) and there shouldn't be a need to copy it to an array; it can be accessed just like an array or using for..of syntax.
There was a problem hiding this comment.
I have been having an issue with this, basically it turns out that Universal uses domino for the DOM interaction, so children normally is an HTMLCollection but it is a ChildrenCollection in Universal (and in some of the Angular's tests as well, that's how I've found the issue)
ChildrenCollection has a different interface and it's not iterable, so unfortunately I will have to add here some code to handle both 😓
There was a problem hiding this comment.
Oh interesting! I was looking for when HTMLCollection became iterable and I think it was with ES2015, but couldn't really find a definitive answer for this. You can always index into them with the item method and an indexed loop.
There was a problem hiding this comment.
cool, thanks, I'll try to look into that 🙂 (as I am really not familiar with domino), one thing I would like to try is to extract the data from either HTMLCollection or ChildrenCollection into an iterable of Element so that they can be treated in the same way, anyways I will need to investigate/play a bit around it 🙂
There was a problem hiding this comment.
I just checked using material and the query actually seem to be working fine there.... I wonder, probably the animations are not kicked in in SSR and this is an issue only with the tests then 🤔 🤷♂️
anyways at this point I feel like I can put this on hold and try to look in the performance thing first 🤔
| return matched; | ||
| } | ||
| } | ||
| return matched; |
There was a problem hiding this comment.
nit: no need to return matched really, given that it is the input parameter. It would mean that the initial call has to become:
const matched = [];
queryFn(element, selector, multi, matched, true);
return matched;Looking at it more, I think you'll want to have a boolean return value that indicates whether the query can terminate (as an alternative to the !multi && matched.length condition). You can then write the loop as
for (const el of element.children) {
if (queryFn(el, selector, multi, matched, false)) {
return true;
}
}and then it doesn't really hurt to duplicate that loop.
There was a problem hiding this comment.
I am not sure, I really don't think that providing the result of functions through the inputs is a nice pattern
We are sort of already cheating by modifying the array, but that is sort of ok because of the performance gain I think, regarding not returning the array I am not sure if that would provide much gain performance wise since retruning and assigning it are based on the array reference, so they shouldn't be problematic.
If we then also need to add a boolean flag in the return it does seem to me to increase the readability of the solution without much gain.
So yeah from my point of view I think you're right that returning the value is not really necessary and can be avoided, but I think that avoiding it just makes the code less readable and I can't see too much gain in doing so 🤔
(By the way, I know that using inputs as outputs is done in other places, for example in the animations package, this is very often used to collect errors, but regardless, I do personally think is a pattern to be avoided if possible)
There was a problem hiding this comment.
My primary motivation to not return the array is that it's slightly misleading, similar to how Array.prototype.sort is misleading: the return value would make me believe that it returns an owned array which is distinct from the input, but that is not the case here.
As far as "collecting" results into an array that is passed as parameter, I believe that is a pretty typical pattern. It's probably more appropriate if the function wasn't named queryFn, but rather collectQueryMatches; then it's much clearer from just looking at the name how the function gathers results.
There was a problem hiding this comment.
You do have a point in the fact that it is slightly misleading
I am still not a big fan of providing the return via an input variable, but I get your point (and it's not like we can clone the array in the function as that would be too expensive 😓) so ok, I'll update the function and its name 🙂👍
There was a problem hiding this comment.
Regarding the boolean, I am not sure, it seems like !multi && matched.length still works fine, do you see some issue there?
There was a problem hiding this comment.
Now that you've extracted the loop out into collectQueryMatchesOnChildren the boolean return is a bit less helpful.
I do think I prefer it like this though, but this may be personal and may be getting into stylistic preference.
function collectQueryMatches(
element: Element, selector: string, multi: boolean, matched: Element[],
isStartElement: boolean): boolean {
const matches = isStartElement ? false : element.matches(selector);
if (matches) {
matched.push(element);
if (!multi) {
return true;
}
}
for (const el of element.children) {
if (collectQueryMatches(el, selector, multi, matched, false)) {
return true;
}
}
if (element.shadowRoot) {
for (const el of element.shadowRoot.children) {
if (collectQueryMatches(el, selector, multi, matched, false)) {
return true;
}
}
}
return false;
}The bigger thing to discover is the perf impact of this overall approach; if an entirely different approach is needed then this is all moot anyway.
There was a problem hiding this comment.
I don't hate your version, I always try to avoid code duplication if possible and that's why I would probably go with my version, but I am happy to compromise and apply yours 🙂 (it's also probably slightly more efficient since there are less function calls involved)
Anyways yeah the performance thing is the biggest issue, I will look into it soon (I sort of wanted to address the other small issues first)
There was a problem hiding this comment.
Ah by the way, I need to add as any as Iterable<Element> to make the for-ofs work 😓
Yeah I totally agree, I did mention this in the PR's description, 100% this is going to be much much less performant than the native querySelector function. My guess is that it shouldn't be too bad, but if you do apply a query in the root of a page full of components this could indeed cause some issues, do you think this can be checked with a presubmit or some sort of automated way? (Regarding the |
I'm afraid the answer is no, but you can create a synthetic benchmark with various DOM tree sizes/depths, shadow roots present or not, etc, and then compare the various options. It is very hard to create realistic benchmark setups though and it's hard to avoid bias towards some scenario, but at least it should give an idea how the various approaches behave (my concern about performance is only speculative at the moment, I do not have any data to back it up).
I have no clue of the performance characteristics of either approach ( |
address JoostK newer comments and improve code
get rid or collectQueryMatchesOnChildren function
|
So @JoostK regarding the performance I have added some One experiment was to add a trigger at the root of the application which query would basically go through the entire dom (in the material page I tested, the button one, there are almost 700 dom elements). With that query I could see some different in numbers, but quite small, the new query took ~4.3 ms vs the querySelectorAll which took ~0.4 ms. Then I went through other pages and usually the 2 query versions do take a similar amount of time (with the new one being slightly slower), with some spikes here and there for some reason. (the first log is the root query experiment, and after that I just clicked around trying to see what animates) I put the results in (kinda) excel and this is the result I got: As you can see the means are very similar but there can be quite slower queries with the new implementation, although always in the order of a few milliseconds. This is a first benchmarking tests I though of, it is a bit up in the air (I did not plan what to test, etc...) but it does show some results (I can do other tests if you want, and if you have suggestions please let me know 🙂), what do you think of them? |
|
Thanks for sharing the results! The 10x initial regression seems bad, but I wonder if it's due to a cold VM. There are a couple more significant outliers that are a bit worrying though. Do you happen to have CPU profiles as well? |
|
By "cold VM" I mostly mean non-optimized code paths; the VM needs to gather some statistics before being able to obtain the best level of optimizations and this is needed per code path; only once a code path has been run sufficiently many times would more optimum code be generated. So even though there is a 5s wait time, the quering logic might still be cold if it hasn't been exercised before. 4ms of a render cycle is about 25% of the frame budget, so it's quite a bit IMO. I realise those are the worst case figures you've observed for a full DOM scan, but a 10x regression is significant. |
Thanks for the explanation, I did misunderstood what you meant 🙂 Unfortunately anyway no, the difference is around ~10x even if that query code was already run |
|
I do understand what you mean about it being significant, anyways this is functionality which simply does not work. I can try with On the other hand I am not even sure how many people use the shadowDom and how many use it alongside animations. So I am not sure this may not even be worth it, but if not maybe this should be properly communicated to devs. (Like for example putting in the docs that the animations are not guaranteed to work alongside the shadowDom or something). What do you think? 🤔 I would also be curious to know what @jessicajaniuk thinks of it 🙂 |
|
I just looked at the Blink source code, and they do indeed implement a fast path for class-only queries: It still does a full traversal though just like we do "manually" now. A CPU profile might indicate where the bottleneck occurs; either in walking the DOM (in which case |
|
I got lost in the discussion at some point, but I just wanted to throw this module out there https://github.com/salesforce/kagekiri. It does |
|
Ah, interesting, I've never seen that lib. I wonder now if we need support for CSS selectors that span across shadow roots; I doubt that the animations engine needs it (it only queries for simple class names, I believe) but the main question is whether using |
|
@crisbeto thank you so very much, I'll have a look and see how that lib fares 🙂 Anyways it seems like a barely maintained library which has only a handful of developers, I think it would be a bit too risky to include it as a dependency in the angular core lib, wouldn't it? @JoostK regarding your comment about piercing or not shadow doms, if an application were to use shadowDom view encapsulation I can definitely see this hindering quite a bit the use of the animations, but again, I am not sure how much shadowDom is actually used The other thing that I would like to mention/ask, is what the emulated view encapsulation is supposed to be, is it assumed to be its own thing? or should it be a view encapsulation which is as close as possible to the real shadowDom one? |
Add a section regarding component view encapsulations in the complex animation sequences guide to let developers know how animations work in regard to view encapsulations. This section is used to both add information to the guide but also to take a stand on how the animations should behave in regard to the shadow dom view encapsulation. This relates to PR angular#46488 which by trying to make sure the query function works with shadow dom elements proved that having the animations implementation work with shadow dom would increase the complexity of the code and would have a negative impact on performance, such facts alongide the small adoption of shadow dom by users influenced the decision to keep the current functionalities and simply discourage the use of animations and shadow dom components.
|
@JoostK and @crisbeto Thank you so very much for having a look at the PR 🙂 I had a chat with @jessicajaniuk and we concluded that it is probably not worth increasing the complexity of the code and decrease performance in order to have the animations work with shadow dom but it is better to simply say that the animations implementation hasn't been designed to work with shadow dom and discourage users from using both together. So I added a section in the docs about this: #46738 Please let me know what you think, hopefully you agree with the decision 🙂 |
…#46738) Add a section regarding component view encapsulations in the complex animation sequences guide to let developers know how animations work in regard to view encapsulations. This section is used to both add information to the guide but also to take a stand on how the animations should behave in regard to the shadow dom view encapsulation. This relates to PR #46488 which by trying to make sure the query function works with shadow dom elements proved that having the animations implementation work with shadow dom would increase the complexity of the code and would have a negative impact on performance, such facts alongide the small adoption of shadow dom by users influenced the decision to keep the current functionalities and simply discourage the use of animations and shadow dom components. PR Close #46738
…#46738) Add a section regarding component view encapsulations in the complex animation sequences guide to let developers know how animations work in regard to view encapsulations. This section is used to both add information to the guide but also to take a stand on how the animations should behave in regard to the shadow dom view encapsulation. This relates to PR #46488 which by trying to make sure the query function works with shadow dom elements proved that having the animations implementation work with shadow dom would increase the complexity of the code and would have a negative impact on performance, such facts alongide the small adoption of shadow dom by users influenced the decision to keep the current functionalities and simply discourage the use of animations and shadow dom components. PR Close #46738
|
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |




fix the query function not taking into consideration element inside
components using the shadowDom view encapsulation
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
Issue Number: N/A
The
queryfunction does not check elements inside shadow roots (which are created with the SahdowDom view encapsulation)For example:

What is the new behavior?
Elements inside shadow roots are also being considered
For example:

Does this PR introduce a breaking change?
Other information