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

Jak*_*son 58 html javascript html5 mutation-observers

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

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

一些HTML

<body>

  ...

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

  ...

</body>
Run Code Online (Sandbox Code Playgroud)

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
});
Run Code Online (Sandbox Code Playgroud)

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

这是一个问题,如果我想不那么具体,并注意<li>整个HTML身体,如下所示:

var container = document.querySelector('body');
Run Code Online (Sandbox Code Playgroud)

我知道它适用于我为自己设置的基本示例......但是不建议这样做吗?这会导致性能不佳吗?如果是这样,我将如何检测和衡量性能问题?

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

wOx*_*xOm 126

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

特别是,如果页面开始加载之前观察员附后(即document_start/ document-start在Chrome扩展/ WebExtensions/userscripts或只是在内部的正常同步网页脚本<head>),同时也对堆积如山的动态更新的网页,例如分支比较GitHub上.未经优化的MutationObserver回调可如果页面是大而复杂的几秒钟添加到网页加载时间(1,2).大多数示例和现有库都不考虑这种情况,并提供外观漂亮,易于使用但速度慢的js代码.

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

  1. 始终使用devtools探查器并尝试使观察者回调消耗不到页面加载期间消耗的总CPU时间的1%.

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

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

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

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

  6. 当只等待一个具有id属性的元素时,使用疯狂快速getElementById 而不是枚举mutations数组(它可能有数千个条目):示例.

  7. 如果所需元素在页面上相对罕见(例如iframeobject),则使用由getElementsByTagName和返回的实时HTMLCollection ,getElementsByClassName并重新检查它们,而不是枚举mutationsif是否具有超过100个元素.

  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. 如果针对非出血边缘浏览器,请不要使用慢速ES2015循环,例如for (let v of something)MutationObserver回调内部,除非您进行转换,以便生成的代码运行速度与经典for循环一样快.

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

回到问题:

观看一个非常特定的容器ul#my-list,看看是否<li>附加了任何容器.

既然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});
Run Code Online (Sandbox Code Playgroud)

  • 显然我不能立即奖励赏金,但我给这个答案奖励 50 分,因为我发现观看稀有元素的实时 DOM 收集技巧很聪明!好答案! (2认同)
  • `const mo = new MutationObserver(asyncmutations =&gt; { // ... });` 也是避免在突变观察者回调期间 DOM 更改的好方法。 (2认同)