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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/31659567/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 06:59:25  来源:igfitidea点击:

Performance of MutationObserver to detect nodes in entire DOM

javascripthtmlmutation-observers

提问by Jake Wilson

I'm interested in using MutationObserverto 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.

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

All the MutationObserverexamples I've seen so far only detect if a node is added to a particular container. For example:

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

some HTML

一些 HTML

<body>

  ...

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

  ...

</body>

MutationObserverdefinition

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

So in this example, the MutationObserveris setup to watch a very certain container (ul#my-list) to see if any <li>'s are appended to it.

所以在这个例子中,MutationObserveris 设置为观察一个非常确定的容器 ( ul#my-list) 以查看是否有任何<li>'s 附加到它。

Is it a problem if I wanted to be less specific, and watch for <li>'s over the entire HTML body like this:

如果我想不那么具体,并<li>像这样在整个 HTML 正文中观察's是否有问题:

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 MutationObserverexamples are so specific with their targeted container... but I'm not sure.

我想可能有一个原因,所有MutationObserver示例都针对其目标容器如此具体……但我不确定。

回答by wOxxOm

This answer applies to big and complex pages.

这个答案适用于大而复杂的页面。

Especially if an observer is attached before page starts loading (that is document_start/document-startin 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.

特别是如果在页面开始加载之前附加了观察者(即document_start/document-start在 Chrome 扩展程序/WebExtensions/userscripts 中或只是在正常的同步页面脚本中<head>),但也适用于庞大的动态更新页面,例如 GitHub 上的分支比较。如果页面又大又复杂 ( 1, 2) ,则未优化的 MutationObserver 回调可能会使页面加载时间增加几秒钟。大多数示例和现有库都没有考虑到此类场景,并提供了好看、易于使用但速度较慢的 js 代码。

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.

MutationObserver 回调作为阻止 DOM 进一步处理的微任务执行,并且可以在复杂页面上每秒触发数百或数千次。

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

  2. Avoid triggerring forced synchronous layoutby 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 documentrecursively, disconnect the observer on success, attach a new nonrecursive one on this container element.

  6. When waiting for just one element with an idattribute, use the insanely fast getElementByIdinsteadof enumerating the mutationsarray (it may have thousands of entries): example.

  7. In case the desired element is relatively rare on the page (e.g. iframeor object) use the live HTMLCollection returned by getElementsByTagNameand getElementsByClassNameand recheck them all insteadof enumerating the mutationsif it has more than 100 elements, for example.

  8. Avoid using querySelectorand especially the extremely slow querySelectorAll.

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

  10. 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 of addedNodesin 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 * xso 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.

  11. If targeting non-bleeding edge browsers, don't use the slow ES2015 loopslike for (let v of something)inside MutationObserver callback unless you transpile so that the resultant code runs as fast as the classic forloop.

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

  1. 始终使用devtools 分析器,并尝试使您的观察者回调在页面加载期间消耗的总 CPU 时间少于 1%。

  2. 避免通过访问 offsetTop 和类似属性触发强制同步布局

  3. 避免使用像 jQuery 这样复杂的 DOM 框架/库,更喜欢原生 DOM 的东西

  4. 观察属性时,使用中的attributeFilter: ['attr1', 'attr2']选项.observe()

  5. 只要有可能,非递归地观察直接父母 ( subtree: false)。
    例如,通过document递归观察来等待父元素,成功时断开观察者,在这个容器元素上附加一个新的非递归是有意义的。

  6. 当只等待一个具有id属性的元素时,使用非常快的方法getElementById而不是枚举mutations数组(它可能有数千个条目):example

  7. 例如,如果所需元素在页面上相对较少(例如iframeobject),请使用getElementsByTagName和返回的实时 HTMLCollectiongetElementsByClassName并重新检查它们,而不是枚举mutations100 多个元素。

  8. 避免使用querySelector,尤其是极慢的querySelectorAll

  9. 如果querySelectorAll在 MutationObserver 回调中绝对不可避免,则首先执行querySelector检查,如果成功,则继续querySelectorAll。平均而言,这样的组合会快得多。

  10. 如果针对非出血边缘浏览器,请不要使用需要回调的内置数组方法,例如 forEach、filter 等,因为在 Chrome 的 V8 中,与经典for (var i=0 ....)循环相比,调用这些函数的成本一直很高(慢 10-100 倍) ,但 V8 团队正在研究它 [2017]),并且 MutationObserver 回调可能每秒触发 100 次,addedNodes在复杂的现代页面上的每批突变中有数十、数百或数千个。

    数组内置函数的内联不是通用的,它通常发生在类似基准测试的原始代码中。在现实世界中,MutationObserver 具有间歇性的活动峰值(例如 1-1000 个节点每秒报告 100 次),并且回调从来没有return x * x这么简单,因此代码没有被检测为“热”到足以进行内联/优化。

    不过,由 lodash 或类似的快速库支持的替代功能枚举是可以的。从 2018 年开始,Chrome 和底层 V8 将内联标准数组内置方法。

  11. 如果针对非流血边缘浏览器,请不要使用像MutationObserver 回调中那样的慢速 ES2015 循环,for (let v of something)除非您进行转译以使生成的代码与经典for循环一样快地运行。

  12. 如果目标是改变页面的外观,并且您有一种可靠且快速的方法来告诉添加的元素在页面的可见部分之外,请断开观察者并通过以下方式安排整个页面的重新检查和重新处理setTimeout(fn, 0):它将在解析/布局活动的初始爆发已完成,引擎可以“呼吸”,这甚至可能需要一秒钟。然后,例如,您可以使用 requestAnimationFrame 以块的形式不显眼地处理页面。

Back to the question:

回到问题:

watch a very certain container ul#my-listto see if any <li>are appended to it.

观察一个非常确定的容器ul#my-list,看看是否有任何<li>附加到它。

Since liis a direct child, and we look for added nodes, the only option neededis childList: true(see advice #2 above).

由于li是直接子节点,并且我们寻找添加的节点,因此唯一需要的选项childList: true(参见上面的建议 #2)。

new MutationObserver(function(mutations, observer) {
    // Do something here

    // Stop observing if needed:
    observer.disconnect();
}).observe(document.querySelector('ul#my-list'), {childList: true});