MutationObserver 检测整个 DOM 中节点的性能

我感兴趣的是使用 MutationObserver来检测某个 HTML 元素是否添加到 HTML 页面的任何位置。例如,我想说我想检测是否在 DOM 中的任何地方添加了任何 <li>

到目前为止,我看到的所有 MutationObserver示例都只检测节点是否被添加到特定容器中。例如:

一些 HTML

<body>


...


<ul id='my-list'></ul>


...


</body>

MutationObserver定义

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

因此在这个示例中,MutationObserver被设置为监视一个非常特定的容器(ul#my-list) ,以查看是否有任何 <li>附加到它。

如果我想成为 没那么具体,并在整个 HTML 主体上观察 <li>,这是一个问题吗:

var container = document.querySelector('body');

我知道它在我为自己设置的基本示例中起作用... ... 但是难道不建议这样做吗?这会导致性能下降吗?如果是这样,我将如何检测和度量性能问题?

我想也许有一个原因,所有的 MutationObserver例子是如此具体与他们的目标容器... 但我不确定。

44365 次浏览

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.

  1. Always use the devtools profiler and try to make your observer callback consume less than 1% of overall CPU time consumed during page load.

  2. Avoid triggerring forced synchronous layout by accessing offsetTop and similar properties

  3. Avoid using complex DOM frameworks/libraries like jQuery, prefer native DOM stuff

  4. When observing attributes, use attributeFilter: ['attr1', 'attr2'] option in .observe().

  5. Whenever possible observe direct parents nonrecursively (subtree: false).
    For example, it makes sense to wait for the parent element by observing document recursively, disconnect the observer on success, attach a new nonrecursive one on this container element.

  6. When waiting for just one element with an id attribute, use the insanely fast getElementById instead of enumerating the mutations array (it may have thousands of entries): example.

  7. In case the desired element is relatively rare on the page (e.g. iframe or object) use the live HTMLCollection returned by getElementsByTagName and getElementsByClassName and recheck them all instead of enumerating the mutations if it has more than 100 elements, for example.

  8. Avoid using querySelector and especially the extremely slow querySelectorAll.

  9. If querySelectorAll is absolutely unavoidable inside MutationObserver callback, first perform a querySelector check, and if successful, proceed with querySelectorAll. On the average such combo will be a lot faster.

  10. 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.
  1. 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 classic for loop.

  2. 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.

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