问题
I'm interested in using MutationObserver
to detect if a certain HTML element is added anywhere in an HTML page. For example's sake, I'll say that I want to detect if any <li>
's are added anywhere in the DOM.
All the MutationObserver
examples I've seen so far only detect if a node is added to a particular container. For example:
some HTML
<body>
...
<ul id='my-list'></ul>
...
</body>
MutationObserver
definition
var container = document.querySelector('ul#my-list');
var observer = new MutationObserver(function(mutations){
// Do something here
});
observer.observe(container, {
childList: true,
attributes: true,
characterData: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true
});
So in this example, the MutationObserver
is setup to watch a very certain container (ul#my-list
) to see if any <li>
's are appended to it.
Is it a problem if I wanted to be less specific, and watch for <li>
's over the entire HTML body like this:
var container = document.querySelector('body');
I know it works in the basic examples I've setup for myself... But is it not advised to do this? Is this going to result in poor performance? And if so, how would I detect and measure that performance issue?
I figured maybe there was a reason that all the MutationObserver
examples are so specific with their targeted container... but I'm not sure.
回答1:
This answer applies to big and complex pages.
Especially if an observer is attached before page starts loading (that is document_start
/document-start
in Chrome extensions/WebExtensions/userscripts or just in a normal synchronous page script inside <head>
), but also on humongous dynamically updated pages e.g. branch compare on GitHub. An unoptimized MutationObserver callback can add a few seconds to page load time if the page is big and complex (1, 2). Most of the examples and existing libraries don't account for such scenarios and offer good-looking, easy to use, but slow js code.
MutationObserver 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.
Always use the devtools profiler and try to make your observer callback consume less than 1% of overall CPU time consumed during page loading.
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 non-bleeding edge browsers, don't use 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, but V8 team is working on it [2017]), and MutationObserver callback may fire 100 times per second with dozens, hundreds or thousands ofaddedNodes
in each batch of mutations on complex modern pages.Inlining of array built-ins isn't universal, it generally happens in benchmark-like primitive code. In the real world MutationObserver has intermittent spikes of activity (like 1-1000 nodes reported 100 times per second) and the callbacks are never as simple as
return x * x
so the code is not detected as "hot" enough to be inlined/optimized.The alternative functional enumeration backed by lodash or similar fast library is okay though. As of 2018 Chrome and the underlying V8 will inline the standard array built-in methods.
If targeting non-bleeding edge 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.
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});
来源:https://stackoverflow.com/questions/31659567/performance-of-mutationobserver-to-detect-nodes-in-entire-dom