MutationObserver对于检测整个DOM中的节点性能如何?

119

我想使用 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 body 中的 <li>,这会有问题吗:

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

我知道在我为自己设置的基本示例中它可以正常工作...但是这样做不被建议吗?这会导致性能差吗?如果有,我该如何检测和测量性能问题?

我认为也许所有的MutationObserver示例都有特定的目标容器,可能是因为某种原因...但我不确定。


性能问题是针对特定场景的。如果您的整个文档只有几个简单元素,我相信您不会遇到任何问题。如果您担心性能问题,请进行性能分析! - Amit
11
我已经使用了大量的 MutationObserver 并且让它们递归地监视整个 DOM。就我个人而言,我从未遇到过性能问题。 - Luke
9
引入MutationObservers并弃用MutationEvents的主要原因是MutationObservers能够聚合变更,因此速度更快。我们还会在大型文档上使用带有subtree: true的MutationObservers,但从未出现过问题。 - loganfsmyth
1
你为什么要观察属性和字符数据的变化?你自己说了——你想观察可能添加的 li 元素?如果某些东西可能导致次优性能,那么我认为请求比你需要的更多事件就是其中之一。 - Armen Michaeli
例如,Boomerang.js(https://github.com/akamai/boomerang)是一个用于监测网页性能的库,它使用`MutationObserver`来测量单页应用程序的页面加载时间。 - JulienD
如果你只想检测DOM中是否添加了任何<li>元素,那么你不需要使用attributes: true, characterData: true, attributeOldValue: true, characterDataOldValue: true - Chester Fung
1个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
336

本答案主要适用于大型和复杂的页面。

如果在页面加载/渲染之前附加,未经优化的MutationObserver回调函数可能会导致页面加载时间延长几秒钟(例如,从5秒到7秒),特别是在处理大型和复杂页面时(12)。该回调函数作为一个微任务执行,会阻塞DOM进一步处理,并可能在复杂页面上每秒触发数百次甚至数千次。大多数示例和现有库都没有考虑这种情况,并提供外观优美、易于使用但潜在缓慢的JS代码。

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

  2. 避免通过访问offsetTop等属性来触发强制同步布局

  3. 避免使用像jQuery这样的复杂DOM框架/库,而是使用本地的DOM方法。

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

  5. 尽可能仅对直接父级进行非递归观察(subtree:false)。
    例如,通过递归观察document等待父元素是有意义的,在成功后断开观察器连接,然后在此容器元素上附加一个新的非递归观察器。

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

  7. 如果所需元素在页面上相对罕见(例如iframeobject),请使用getElementsByTagNamegetElementsByClassName返回的实时HTMLCollection,并重新检查它们所有而不是枚举mutations,例如如果它有超过100个元素。

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

  9. 如果MutationObserver回调中绝对无法避免使用querySelectorAll,请先进行querySelector检查,如果成功,则继续使用querySelectorAll。平均而言,这种组合将更快。

  10. 如果针对早于2018年的Chrome / ium,请勿使用内置的Array方法(如forEach,filter等)需要回调,因为在Chrome的V8中,与经典的for(var i = 0 ... ....)循环相比,这些函数始终很难调用(速度慢10-100倍),并且MutationObserver回调可能会报告复杂现代页面上的数千个节点。

  • 即使在旧版浏览器中,由lodash或类似快速库支持的替代功能枚举也可以使用。
  • 截至2018年,Chrome / ium正在内联标准数组内置方法。
  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.

回到问题:

监视一个特定的容器 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});

3
显然我不能立刻发放奖励,但我将为这个答案提供50点赏金,因为我觉得动态 DOM 集合技巧对于监视罕见元素很聪明!好答案! - Benjamin Gruenbaum
1
@Bergi,这是针对处理过程复杂、耗时长的情况。MutationObserver在微任务队列中运行,因此多个回调可能仍然驻留在一个任务中,这可能导致非常长的绘制帧和卡顿。 - wOxxOm
@mmm,有些网站可能会将整个容器与树中的新渲染元素交换,因此实际上在这种情况下,您将无法使用此优化。 如果您想进一步讨论此事,请使用 [MCVE](/help/mcve) 打开一个新问题。 - wOxxOm
1
这是一个非常好的解释。然而,在你的最终答案中,你应该遵循第五点 :) 使用document.getElementById('my-list')代替document.querySelector('ul#my-list') - jfudman
@jfudman,这个答案涉及到观察者回调,因为理论上它可能会运行数千次。在这个例子中找到父级是一次性的事情,所以除非有人观察数百个不同的父级创建数百个观察者,否则没有必要进行优化。 - wOxxOm
显示剩余9条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,