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; }
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});