-
Notifications
You must be signed in to change notification settings - Fork 294
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
MutationObserver with querySelector for elements #77
Comments
So could you perhaps clarify the proposal a bit. How should adding an attribute work? what about removal? At which point should the selector run in each case? |
Oh, silly me, you want selector based filtering for child nodes only, not attributes (the example just happens to do something with attributes too). But similar question still: at which point should the selector run, especially in case of child node removal? |
I don't know, and I don't think it matters that much. For all cases I can imagine, the only reason I want to observe removals is to clean up my own objects, so I just need to know that an element with certain @smaug---- Does it answer your question? |
And the reason I have both attributes and childNodes is that usually, when the task is to "cover nodes with certain attribute", then I will need to know when they are added/removed and when an already existing node has this attribute added/removed/modified. Which ends up looking like the example, and I think it's fine and allows for fairly good level of customization. |
It does matter quite a bit when the selector runs. When adding a child node selector can run Removing a node would require, I guess, running selector before any DOM mutation has happened. How to make this all perform well? selector matching isn't exactly cheap. But, do we want to use selectors. I think I'd be more comfortable with "raw DOM" data. var filter = ['data-l10n-id', 'data-l10n-args']; That would filter out child nodes which don't have the relevant attribute, even in case innerHTML is used. |
It might be simpler to have a separate selector observer mechanism, i.e. something that just watches for changes to the set of elements in a subtree that match the given selectors, and not be a filter on the existing fields on a MutationRecord. Then we can just require the mutation observer records be generated whenever the element is restyled, and use the restyle process to do the "element now matching" / "element no longer matching" checks. It would mean that a sequence like:
wouldn't generate a record, while:
would generate two (and possibly be coalesced? I think the way MutationObserver works allows for this kind of coalescing). I don't think selector observers need to be more expensive than the selector matching we do for restyles, especially if we're modelling whether an element matches one of these selectors or not is done by pretending we have a style rule in the document with the relevant selector (and some hack to make it rooted at the root of the observation). I think the performance impact of adding a real style rule to a document being about the same as one for selector observers is reasonable. The observer probably needs to maintain a set of elements that currently match, so that when an element is removed from the tree we remove it (and its descendants) from the set. We'd also need to do something different for elements in display:none subtrees, which Gecko doesn't give styles unless script does a getComputedStyle, and we'd probably need to adjust the restyle process to get that to work... |
I don't think we want to use CSS selectors for filtering purposes. Evaluating selectors every time a node is added, removed, or modified is going to be unacceptably expensive.
That would expose the timing of styling recalc. Since that's an area where different browsers optimize things differently, designing an interoperable API that doesn't degrade performance on all browsers is going to very hard. In addition, all the use cases I've seen for filtering mutation records so far involved filtering nodes based on element names or attribute names (or combinations of the both in some rare cases). Given that I agree with @smaug---- that DOM-based approach is better. Also, it's not necessary for the filtering to be perfect. If we can weed out, say, 90% of all other nodes, then scripts can quite efficiently pick the one they need after that. |
Ah, indeed we may want to filter also based on nodeNames. so something like But perhaps for now we could just add attribute name based filter for child nodes. |
Totally agree. :) All my use cases are about attribute based system. I can imagine HTML shims to use node filter as well, but we can probably do without full query selector for now. |
I question this premise. Do we have any data, e.g. from a custom build of a popular rendering engine, to show this to be the case? |
Well, we would create a lot less garbage/cycle collectable objects if browser engine would do filtering. |
I have no doubt about the theoretical benefits. (Indeed, for pretty much any API that allows authors to do things in JavaScript, you can say, "if we moved some of this to C++/Rust, it would generate less garbage.") I am questioning whether it actually shows up in real-world scenarios. |
I instrumented Firefox OS localization framework that localizes nodes registered with Launching a single app (Settings) gave me:
Navigating around the app (between 5 panels) gave me:
The additional problem we face is that the addedNode may be in fact a DOMFragment and we have to find nodes with I just started working on a counter part to that API that brings Intl capabilities for elements. It will use And we have a shim for a color picker that only wants to register input[type=color], but has to listen to all mutations. Does it bring an examples you've been looking for @domenic ? |
I don't know what you mean with DOMFragment, but you would still need to still look for |
@zbraniecki thanks for investigating, but that wasn't quite what I was wondering. I was wondering about speed (in seconds) for filtering in C++, as this issue proposes, vs. filtering in JS using suitably-efficient strategies (so e.g. looping and checking el.localName instead of using querySelector). It's not worth it to add an API to the platform if the only benefit is less "noise" to the mutation observer callbacks; you can write a trivial wrapper library to handle that. The benefit would be if there are real performance advantages, which is why I was hoping to see benchmarks. |
I launched a light-workload marionette test scenario on the app:
that's signal/noise ratio of 0.14. That's a lot of resources going nowhere. |
Gotcha. Ok. I'd need help from someone with platform experience to write a shim for me and I'll be happy to test that! |
But also, notice that it's not only about speed in seconds. It's also about the amount of allocations and matching GCs. My naive understanding is that there are four benefits:
Wrapper could help with 1), you are asking about 2). Do we agree on 3 and 4? |
Well, 3) is just the same as 2). But yes, 2/3 and 4 are both worth investigating. I guess my issue is that 4 is a general problem with every web API ever; if you move portions of it into C++, there are less allocations and GCs. In the end why don't we just write all our code in webasm... |
Yeah, I see your point. I believe that the problem with MutationObserver is that most use cases I saw in our platform are the opposite of what MutationObserver API is designed for. It seems to be designed to help in cases where the mutation observer callback is interested in vast majority of nodes under given root element. I see MutationObserver as a tool for scenarios where the number of nodes of interest compared to amount of "traffic" is very, very low. But MutationObserver is also the only way to properly shim a color picker, file picker, any other widget, mentioned In order to use MutationObserver efficiently as a shim, one would also want to enable it before parser is done with DOM and get the DOM from the Parser filtered. But that increases the number of nodes that can get mutations unless we can improve the filtering. So, while I agree with the concern, I believe that MutationObserver is special here - an API with a great potential to enhance the Web stack, that currently enables doing so at an unreasonable cost. |
Yeah, it makes sense to add one-off hacks for general problems if the payoff is worthwhile. I just think it's worth measuring first. So yeah, we want speed and memory usage benchmarks to show this would be a worthwhile platform feature for every browser to implement. That seems like the bare minimum to get cross-vendor interest. |
I don't think such an investigation is necessary to justify the API. A high noise-to-signal ratio itself is a problem worth solving, and we've always wanted to add a filtering mechanism for MutationObserver from the beginning. We never got around to it because each of us were distracted by other things. In addition, without doing any investigation, I can tell you that creating a JS array with thousands of entries in the browser engine is expensive and will cause a GC churn. The fact MutationObserver records are objects is already bad, and we spent a lot of time minimizing the amount of information being collected for performance. This is why collecting old values for CharacterData and attribute value is optional. There is literally nothing controversial / mysterious here about the cost. |
So I was looking at http://www.w3.org/2008/webapps/wiki/MutationReplacement#NodeWatch_.28A_Microsoft_Proposal.29 again, and discussing with zbraniecki and I think one possible option here is to have a way to observe mutations, without actually creating the MutationRecords. var pending = false; |
This is an early POC of how it might look like: https://bug1214026.bmoattachments.org/attachment.cgi?id=8672860 - I'll try to plug it into our OS and measure impact on performance. If it's good, then we can look into |
zbraniecki, I thing I meet the same problem like yours. new MutationObserver(
function (mutations)
{
mutations.map(
function (mutation)
{
if (mutation.target != '[object HTMLBodyElement]' &&
mutation.target != '[object HTMLScriptElement]' &&
(mutation.removedNodes.length != 0 ||
mutation.addedNodes.length != 0)) {
Languages.update(); //Here is what you want to do
}
}
);
}
).observe(
document.querySelector('body'),
{attributes: false, childList: true, characterData: false, subtree:true}
); |
I still think we should just add a filter based on element name & attribute name but adding an option to not receive individual records also seems like a good idea as well. I think we should do all those things. |
A huge use case for this would be attaching events to elements before DOMContentLoaded. MutationObserver has the potential to fix this very pervasive problem and give developers the power to attach events to elements as they are rendered. However, currently that would require looping over every single element and using If the platform could do most of this filtering efficiently, then using MO for this would be a feasible solution. It doesn’t have to be the entire selector syntax, but at least tags, classes, ids, attributes (partial too), commas. I wonder if we could mine jQuery code (since that has a very specific pattern of |
I'm going to close this on performance grounds as well as it having objections from implementers. #398 is still something we can (and I think should) pursue. Extensions that are similar in scope we could also consider in a new issue, but full selector support is out. The research @LeaVerou suggests seems like a great first step for any such issue. Lifting some of the burden from libraries and frameworks is definitely something implementers are interested in. |
MutationObserver is a great tool for writing small libraries that operate on specific types of elements.
Examples:
** Date/Time pickers on
<input type="datetime">
** Additional API on some types of
<link>
elements** Libraries that aid with specific
aria-*
values** Libraries that handle localization of
data-l10n-*
elementsCurrently, MutationObserver API is designed to aid code that wants to operate on all subtree operations of a given root.
But in all those cases the basic premise is that they want to handle specific set of elements. Their additions/removals and possibly modifications.
The result is that in all those cases that I encountered in my work there's a massive amount of noise generated because the library is fishing for a particular element type, while it has to retrieve all node additions/removals and filter them out manually.
In many cases, the ratio of nodes inserted (dynamically or by the parser) to elements the library wants to operate on is 100/1.
My understanding is that the platform is in much better position to optimize filtering the elements for MutationObserver than anything that the MutationObserver callback can do.
Additionally, even if the code logic would be the same, the sole fact that the
mutations
argument has to be constructed and then the callback for them has to be called, for many mutations that are not relevant to the MutationObserver is a waste of CPU time and memory/GC allocation.My initial proposal would look something like this:
The text was updated successfully, but these errors were encountered: