Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

API request: MutationObserver with querySelector

169 views
Skip to first unread message

Zibi Braniecki

unread,
Sep 17, 2015, 11:30:46 PM9/17/15
to
Hi,

One of the major use cases for MutationObserver is all kinds of libraries that either shim APIs or provide intrinsic modifications to DOM experience.

Examples of such libraries may be:

* A library that provides Date/Time pickers only caring about <input type="date|time">
* A library that extends behavior of a particular web component from outside
* A library that extends behavior of common elements with particular attributes
* our l10n library only looks for element with data-l10n-id|data-l10n-args
* a resource API shim may be looking for <links> with a given attribute

Unfortunately, at the moment, MutationObserver API makes it particularly hard for libraries to narrow down the scope of elements that are monitored by them which results in three things:

*) Higher CPU/power cost of running a MutationObserver on a DOM tree
*) More noise inside the MutationObserver callback
*) Requirement for fairly sophisticated filtering to get the right elements

The reason for so much noise is that there's no way to instrument MutationObserver to notice only specific elements. The only filtering can be done for attributes, but for node adding/removing MutationObserver reports *all* elements and often in form of a DOMFragment which has to be filtered.

Example from our l10n library:

const observerConfig = {
attributes: true,
characterData: false,
childList: true,
subtree: true,
attributeFilter: ['data-l10n-id', 'data-l10n-args']
};

var mo = new MutationObserver(onMutations);
mo.observe(this.doc, observerConfig);

function onMutations(mutations) {
const targets = new Set();

for (let mutation of mutations) {
switch (mutation.type) {
case 'attributes':
targets.add(mutation.target);
break;

case 'childList':
for (let addedNode of mutation.addedNodes) {
if (addedNode.nodeType === addedNode.ELEMENT_NODE) {
if (addedNode.hasAttribute('data-l10n-id')) {
targets.add(addedNode);
}
if (addedNode.childElementCount) {
element.querySelectorAll('[data-l10n-id]').forEach(
elem => targets.add(elem));
}
}
}
break;
}
}

if (targets.size === 0) {
return;
}

translateElements(targets);
}

Proposal:

const observerConfig = {
attributes: true,
attributeFilter: ['data-l10n-id', 'data-l10n-args']
querySelector: '*[data-l10n-id]'
};

var mo = new MutationObserver(onMutations);
mo.observe(this.doc, observerConfig);

function onMutations(mutations) {
const targets = new Set();

for (let mutation of mutations) {
switch (mutation.type) {
case 'attributes':
targets.add(mutation.target);
break;

case 'childList':
for (let addedNode of mutation.addedNodes) {
targets.add(addedNode);
}
break;
}
}

if (targets.size === 0) {
return;
}

translateElements(targets);
}


And it's not only about cleaner code. In Firefox OS' Settings app, our onMutations is fired hundreds of times as we construct DOM dynamically, while l10n cares about 70 elements out of those.

I believe that there's a substantial value in extending MutationObserver API to help with such filtering for all scenarios listed at the top and many similar ones.

Would it be possible to do within MutationObserver API or would it be material for a separate API?

Thanks,
zb.

smaug

unread,
Sep 18, 2015, 8:09:33 AM9/18/15
to Zibi Braniecki
I don't understand what querySelector would filter here. We're observing only attributes but then what...


>
> var mo = new MutationObserver(onMutations);
> mo.observe(this.doc, observerConfig);
>
> function onMutations(mutations) {
> const targets = new Set();
>
> for (let mutation of mutations) {
> switch (mutation.type) {
> case 'attributes':
> targets.add(mutation.target);
> break;
>
> case 'childList':
> for (let addedNode of mutation.addedNodes) {
> targets.add(addedNode);
> }
Hmm, or are you just missing childList from observerConfig.
How would the filtering work for node removals? Would it need to run the filter right before removal?

Are you thinking that querySelector filtering would map also to all the descendants of the actually added child node?
That would be quite a big change how the API works currently. addedNodes wouldn't be about childNodes anymore, but any descendant. Would it be
also about any removed descendants?


> break;
> }
> }
>
> if (targets.size === 0) {
> return;
> }
>
> translateElements(targets);
> }
>
>
> And it's not only about cleaner code. In Firefox OS' Settings app, our onMutations is fired hundreds of times as we construct DOM dynamically, while l10n cares about 70 elements out of those.
>
> I believe that there's a substantial value in extending MutationObserver API to help with such filtering for all scenarios listed at the top and many similar ones.
>
> Would it be possible to do within MutationObserver API or would it be material for a separate API?

selector based observing/filtering was discussed when MutationObserver API was being designed. Some old documentation here
http://www.w3.org/2008/webapps/wiki/MutationReplacement (note, that documentation is from the era when there were no microtasks yet)
IIRC there were concerns selector based filtering being too slow, and also it is a higher level thing, so first
we wanted a low level API. (one could say even attribute filtering should have been left out)

If you want new web exposed changes to the API, better to file a spec bug https://github.com/whatwg/dom/issues/new
so that also other browser vendors and API users can easily comment the proposal.


But in principle I think some kind of filtering would be nice. Implementing it for node removals would be a bit annoying in Gecko case (since our
internal nsIMutationObserver::ContentRemoved happens after the removal) but it is doable. However it is unclear to me what kind of performance
characteristics we'd get - that is the main concern I have atm. (I'd like to avoid adding APIs which can be easily used in such way that it slows down
all DOM operations dramatically.)


-Olli




>
> Thanks,
> zb.
>

Zibi Braniecki

unread,
Sep 18, 2015, 5:37:11 PM9/18/15
to
On Friday, September 18, 2015 at 5:09:33 AM UTC-7, smaug wrote:
> Hmm, or are you just missing childList from observerConfig.

Yeah, sorry for that! I meant to use childList.

> How would the filtering work for node removals? Would it need to run the filter right before removal?

I guess so. It the removed element matches the querySelector then yes.

> Are you thinking that querySelector filtering would map also to all the descendants of the actually added child node?

No. That's the big part that may be a big saving here. The API's I'm describing care about particular nodes, not their child nodes (unless they match the querySelector of course).

> That would be quite a big change how the API works currently. addedNodes wouldn't be about childNodes anymore, but any descendant. Would it be
> also about any removed descendants?

removedNodes, right? Yeah, I guess that's what I'm thinking. It seems to match the name - added/removed nodes that match the querySelector :)

> IIRC there were concerns selector based filtering being too slow, and also it is a higher level thing, so first
> we wanted a low level API. (one could say even attribute filtering should have been left out)

I understand. My point here is that if the API doesn't do that, customer code has to. In all scenarios where people need particular element type, they will have to filer out for them inside the MutationObserver callback.

The biggest difference from my perspective is that with querySelector you make the customer code simpler, give platform a chance to optimize the code better than customer code can and avoid firing MO callback for *every* node when in fact, there may be one element that matches the selector.

> If you want new web exposed changes to the API, better to file a spec bug https://github.com/whatwg/dom/issues/new
> so that also other browser vendors and API users can easily comment the proposal.

Sure, will do. Wanted to check with this group first for feedback and consolidation of the idea.

> But in principle I think some kind of filtering would be nice. Implementing it for node removals would be a bit annoying in Gecko case (since our
> internal nsIMutationObserver::ContentRemoved happens after the removal) but it is doable.

Apologies for being naive, but that shouldn't be a problem, right? We're not talking here about blocking the removal, just notifying the library that an element that the library is operating on has been removed.

> However it is unclear to me what kind of performance
> characteristics we'd get - that is the main concern I have atm. (I'd like to avoid adding APIs which can be easily used in such way that it slows down
> all DOM operations dramatically.)

Yeah, I totally understand and at the same time, I'm requesting this API precisely because I believe that in the current MutationObserver API it is most likely to lead to heuristics that are wasting a lot of CPU for nothing (collect all of the nodes and fire the observer's callback just so that the callback can filter it out as irrelevant and do nothing).

I guess my biggest point here is that MutationObserver currently is designed to let customers operate on all elements of a given root, while in reality, it is used to operate on a specific, narrowly selected type of elements. If platform doesn't filter it out, then customer code does and it goes with a high cost.

I'll post to the WhatWG github.

Thanks!
zb.

Zibi Braniecki

unread,
Sep 18, 2015, 6:02:30 PM9/18/15
to

Cameron McCormack

unread,
Sep 19, 2015, 3:19:55 AM9/19/15
to smaug, dev-pl...@lists.mozilla.org
smaug:
> But in principle I think some kind of filtering would be nice.
> Implementing it for node removals would be a bit annoying in Gecko
> case (since our internal nsIMutationObserver::ContentRemoved happens
> after the removal) but it is doable. However it is unclear to me
> what kind of performance characteristics we'd get - that is the main
> concern I have atm. (I'd like to avoid adding APIs which can be
> easily used in such way that it slows down all DOM operations
> dramatically.)

We could probably use the same mechanism that we do for restyling,
effectively modelling any registered selector observers as an additional
style sheet rule (that doesn’t have any properties set in it). All of
the data we store in the rule cascade so that we can quickly decide
whether a given element needs to have rule matching run again could be
re-used.

--
Cameron McCormack ≝ http://mcc.id.au/

zbran...@mozilla.com

unread,
Oct 8, 2015, 8:46:53 PM10/8/15
to
We're about to start working on another API for the next Firefox OS, this time for DOM Intl, that will operate on `data-intl-format`, `data-intl-value` and `data-intl-options`.

It would be much easier for us to keep l10n and intl separately and independently, but in the current model we will have two MutationObservers reporting everything that happens on document.body just to fish for elements with those attributes. Twice.

So we may have to introduce a single mutation observer to that handles that for both, which will be a bad design decision but improve performance.

I Reported it a month ago and so far no response. What's my next step to get this in our platform?

zb.

smaug

unread,
Oct 9, 2015, 11:46:38 AM10/9/15
to zbran...@mozilla.com
Let's try to move this forward in the spec level (we can't really implement anything before there is some specification for this).
I added a question to the spec bug.



-Olli

Xidorn Quan

unread,
Oct 9, 2015, 9:34:18 PM10/9/15
to smaug, dev-pl...@lists.mozilla.org
On Sat, Sep 19, 2015 at 5:19 PM, Cameron McCormack <c...@mcc.id.au> wrote:
> We could probably use the same mechanism that we do for restyling,
> effectively modelling any registered selector observers as an additional
> style sheet rule (that doesn’t have any properties set in it). All of
> the data we store in the rule cascade so that we can quickly decide
> whether a given element needs to have rule matching run again could be
> re-used.

That probably works for node insertion, but probably doesn't work for
node removal I guess?

- Xidorn
0 new messages