Performance of MutationObserver to detect nodes in entire DOM
This answer primarily applies to big and complex pages.
If attached before page load/render, an unoptimized MutationObserver callback can add a few seconds to page load time (say, 5 sec to 7 sec) if the page is big and complex (1, 2). The callback is executed as a microtask that blocks further processing of DOM and can be fired hundreds or a thousand of times per second on a complex page. Most of the examples and existing libraries don't account for such scenarios and offer good-looking, easy to use, but potentially slow JS code.
Always use the devtools profiler and try to make your observer callback consume less than 1% of overall CPU time consumed during page load.
Avoid triggerring forced synchronous layout by accessing offsetTop and similar properties
Avoid using complex DOM frameworks/libraries like jQuery, prefer native DOM stuff
When observing attributes, use
attributeFilter: ['attr1', 'attr2']
option in.observe()
.Whenever possible observe direct parents nonrecursively (
subtree: false
).
For example, it makes sense to wait for the parent element by observingdocument
recursively, disconnect the observer on success, attach a new nonrecursive one on this container element.When waiting for just one element with an
id
attribute, use the insanely fastgetElementById
instead of enumerating themutations
array (it may have thousands of entries): example.In case the desired element is relatively rare on the page (e.g.
iframe
orobject
) use the live HTMLCollection returned bygetElementsByTagName
andgetElementsByClassName
and recheck them all instead of enumerating themutations
if it has more than 100 elements, for example.Avoid using
querySelector
and especially the extremely slowquerySelectorAll
.If
querySelectorAll
is absolutely unavoidable inside MutationObserver callback, first perform aquerySelector
check, and if successful, proceed withquerySelectorAll
. On the average such combo will be a lot faster.If targeting pre-2018 Chrome/ium, don't use the built-in Array methods like forEach, filter, etc. that require callbacks because in Chrome's V8 these functions have always been expensive to invoke compared to the classic
for (var i=0 ....)
loop (10-100 times slower), and MutationObserver callback may report thousands of nodes on complex modern pages.
- The alternative functional enumeration backed by lodash or similar fast library is okay even in older browsers.
- As of 2018 Chrome/ium is inlining the standard array built-in methods.
If targeting pre-2019 browsers, don't use the slow ES2015 loops like
for (let v of something)
inside MutationObserver callback unless you transpile so that the resultant code runs as fast as the classicfor
loop.If the goal is to alter how page looks and you have a reliable and fast method of telling that elements being added are outside of the visible portion of the page, disconnect the observer and schedule an entire page rechecking&reprocessing via
setTimeout(fn, 0)
: it will be executed when the initial burst of parsing/layouting activity is finished and the engine can "breathe" which could take even a second. Then you can inconspicuously process the page in chunks using requestAnimationFrame, for example.If processing is complex and/or takes a lot of time, it may lead to very long paint frames, unresponsiveness/jank, so in this case you can use debounce or a similar technique e.g. accumulate mutations in an outer array and schedule a run via setTimeout / requestIdleCallback / requestAnimationFrame:
const queue = [];
const mo = new MutationObserver(mutations => {
if (!queue.length) requestAnimationFrame(process);
queue.push(mutations);
});
function process() {
for (const mutations of queue) {
// ..........
}
queue.length = 0;
}Note that requestAnimationFrame fires only when the page is (or becomes) visible.
Back to the question:
watch a very certain container
ul#my-list
to see if any<li>
are appended to it.
Since li
is a direct child, and we look for added nodes, the only option needed is childList: true
(see advice #2 above).
new MutationObserver(function(mutations, observer) {
// Do something here
// Stop observing if needed:
observer.disconnect();
}).observe(document.querySelector('ul#my-list'), {childList: true});
Mutation observer emits n number of changes, which makes my other functions slow. Any ways to wait for each iteration of change and then to proceed?
Check out : Performance of MutationObserver to detect nodes in entire DOM
const queue = [];
const mo = new MutationObserver(mutations => {
if (!queue.length) requestAnimationFrame(process);
queue.push(mutations);
});
function process() {
for (const mutations of queue) {
// ..........
}
queue.length = 0;
}
This piece of code worked fine. Excellent answer given in that post. Thank you.
MutationObserver looping on new nodes
Your MutationObserver
callback gets called every time you change the contents of the node, including the times that you do so within the callback itself. Before making changes within the callback, call observer.disconnect()
, then reconnect using observe
after making your changes:
const callback = function(mutationsList, observer) {
function scanning(el) {
observer.disconnect();
// el.style.visibility = "hidden";
var newTextNodes = textNodesUnder(el);
doSomething(newTextNodes);
observer.observe(targetNode, config);
}
};
JavaScript/DOM: MutationObserver to detect changes on `lang` attributes anywhere in the DOM tree
The following code (same as in the question) is indeed working and will detect lang
attribute changes anywhere in the document.
But it is important to know that only lang
attribute changes in the light DOM will be detected, changes in the shadow DOM will NOT be observed.
const observer = new MutationObserver(() => {
console.log('lang attribute has changed');
});
observer.observe(document, {
attributes: true,
attributeFilter: ['lang'],
subtree: true
});
document.querySelector inside MutationObserver: good or bad practice?
A single query for a selector should be very, very, very fast, so I wouldn't expect this to be a problem, but it will depend a lot on the DOM you're using it in. Test your use cases to see if you find a performance problem.
I would at a minimum make it possible to specify where in the DOM to look, rather than looking through the entire thing on every change. That minimizes calls to your mutation observer, and minimizes the amount of searching it does when it's called. If a particular use case has to look through the entire thing, well, then it can use documentElement
, but I'd at least make it possible to avoid that. (There's also no reason to declare element
in a scope above the scope where it's actually used, and "query string" has a specific meaning in web programming that isn't what you're using it for here, so I'd just use "selector" or "selector string".)
Here's an idea of how you might do that:
const waitForElement = async (selector, rootElement = document.documentElement) => {
return new Promise((resolve) => {
const observer = new MutationObserver(() => {
const element = document.querySelector(selector);
if (element) {
observer.disconnect();
resolve(element);
}
});
observer.observe(rootElement, {
childList: true,
subtree: true,
});
});
};
It would probably also make sense to optionally accept an AbortSignal
so you can stop waiting for the element, having it reject with some appropriate "cancelled" error.
Can mutations observer be used to observe the addition and removal of a single node?
No, it is not possible. Mutation observers only observe added child nodes of a target, not the adding or removing of a target node.
const div = document.createElement("div");
div.textContent = "I've been added";
const selfObserver = new MutationObserver((mutList) => {
mutList.forEach((mut) => console.log("self", mut));
})
selfObserver.observe(div, {childList: true});
setTimeout(() => { document.body.append(div); }, 150);
Related Topics
Capture HTML Canvas as Gif/Jpg/Png/Pdf
Communication Between Tabs or Windows
Why Calling Setstate Method Doesn't Mutate the State Immediately
Why Can't I Call a Function Named Clear from an Onclick Attribute
Offsetting an HTML Anchor to Adjust For Fixed Header
How to Get the Data-Id Attribute
How to Read CSS Rule Values With JavaScript
Maximum Call Stack Size Exceeded Error
Jquery Mobile: Markup Enhancement of Dynamically Added Content
How to Get the Selected Radio Button'S Value
Can One Angularjs Controller Call Another
(Change) VS (Ngmodelchange) in Angular
Using Html5 File Uploads With Ajax and Jquery
Find the Closest Ancestor Element That Has a Specific Class