dom
dom copied to clipboard
Proposal: add `childFilter` option to `MutationObserverInit`
Background
The MutationObserverInit has a subtree option that is not usable, unless childList or attributes are also defined.
The controversial part of this choice is that attributes inevitably involve Element only, while childList includes Node of any type.
As result, it is impossible to optimally observe children that are being added, when text, comments, or character data, are being manipulated, in a way or another, by libraries and whatsoever.
Currently ...
The workaround to fulfill an "observe children only" is to append any Element in the DOM and then element.setAttribute('data-what', '') so that a MutationObserver aware of subtree and attributes only, can verify the attribute is data-what and conside that node, instead of parsing every single kind of node that could land on a document/element.
Proposal
~~Add a children boolean property to the MutationObserverInit to indicate that nothing else should bother the callback in charge of screening nodes, otherwise inevitably bloated, and slowed down, by checks such as if (addedNode.nodeType === 1) ... or similar.~~
Please read the improved proposal.
Thanks for considering this improvement to one of the most successful DOM APIs in latter years.
Having both childList and children is confusing. What's the real performance gain over just extra nodes observed? How practical is that case? (would be great to understand better the value of proposal, considering polyfilling etc)
would be great to understand better the value of proposal
~~would be great to have a conversation before a thumb down ;-)~~ but I actually agree with you the children choice is poor, so how about childFilter ?
Symmetry
Accordingly to your reaction, there was no reason to have attributeFilter option neither, because any callback could've simply checked if the attribute name is one that was meant to be observed, but in more than a occasion I've used this great option that helps a lot avoiding repeated checks over and over, and the callback being triggered for no reason whatsoever.
The MutationObserver callback comes with a cost: it requires looping over all records and each record requires a loop over each children, but in highly dynamic pages, and optimized reactive libraries, looping endlessly for every bacsic element.textContent = update is a huge waste of CPU cycles, GC operations, and all for something unintended to observe.
Performance
Take the classic DBMonster demo as example, and try this code in console:
let children = 0;
new MutationObserver(records => {
for (const record of records) {
for (const node of record.addedNodes) {
children += node.nodeType === 1 ? 1 : 0;
}
}
console.log('Desired nodes', children);
}).observe(document, {subtree: true, childList: true});
Every 3rd party code that uses a MutationObserver on the document in search of new elements will be triggered unnecessary times "forever" without actually doing anything ever, because the only changes happening in that case are #text nodes.
And not only every code on the planet that looks for elements only, since these can invalidate the layout, require reflow, repaint, etc, will be invoked for no reason, accessing nodeType is also a quite expensive operation.
Changing the previous observer with this:
let children = 0;
new MutationObserver(records => {
console.time('MutationObserver');
for (const record of records) {
for (const node of record.addedNodes) {
children += node.nodeType === 1 ? 1 : 0;
}
}
console.timeEnd('MutationObserver');
}).observe(document, {subtree: true, childList: true});
Would show execution times between 0 and 2 milliseconds on an intel i7 CPU, and on top of that, if the budget we have for great performance is 16ms, having any ms down for code that doesn't need to run, and doesn't help developer intents, seems like a relatively huge issue, but could we do better?
Proposal
Add a childFilter option that is identical to attributeFilter in the sense that it's an optional array (list) that accepts the kind of child that a developer would like to be notified about:
new MutationObserver(callback)
.observe(document, {
subtree: true,
childList: true,
childFilter: [
Node.ELEMENT_NODE
]
});
Polyfill (not-optimized)
function child(node) {
return this.includes(node.nodeType);
}
const {filter} = Array.prototype;
class BetterMutationObserver extends MutationObserver {
#childFilter = [];
constructor(callback) {
super((records, self) => {
if (this.#childFilter.length) {
records = records.map(record => ({
...record,
addedNodes: filter.call(record.addedNodes, child, this.#childFilter),
removedNodes: filter.call(record.removedNodes, child, this.#childFilter)
}));
}
callback(records, self);
});
}
observe(target, options) {
if ('childFilter' in options)
this.#childFilter = options.childFilter;
return super.observe(target, options);
}
}
This basic implementation, due its simplicity, will still trigger the callback even when no addedNodes or removedNodes were involved, but empty loops are quite cheap, compared with node.nodeType filter dance each time, but an improved version of the polyfill could better check initial options and avoid invoking the callback if it's not needed, and I'd be happy to provide a better polyfill whenever there's interest in this proposal.
Thank you for considering the childFilter improvement.
Yes, childFilter, or maybe nodeFilter (to be consistent with attributeFilter) looks like meaningful option - would simplify many use-cases and improve performance.
I'm more than open to decide any xFilter naming convention, as long as the feature request is clear :+1: