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

IT技术 javascript html mutation-observers
2021-01-21 04:52:06

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

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

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

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

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

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

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

1个回答

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

如果在页面加载/渲染之前附加,未优化的 MutationObserver 回调可以在页面大而复杂的情况下(例如,5 秒到 7 秒)增加几秒钟的页面加载时间(1 , 2)。回调作为阻止 DOM 进一步处理的微任务执行,并且可以在复杂页面上每秒触发数百或数千次。大多数示例和现有库都没有考虑到这种情况,并提供了好看、易于使用但可能会很慢的 JS 代码。

  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并重新检查它们,而不是枚举mutations超过 100 个元素的 。

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

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

  10. 如果针对 2018 年之前的 Chrome/ium,请不要使用需要回调的内置数组方法,例如 forEach、filter 等,因为在 Chrome 的 V8 中,与经典for (var i=0 ....)循环(10-100倍慢),并且 MutationObserver 回调可能会在复杂的现代页面上报告数千个节点。

  • 即使在较旧的浏览器中,由 lodash 或类似的快速库支持的替代功能枚举也可以。
  • 截至 2018 年,Chrome/ium 正在内联标准数组内置方法。
  1. 如果针对 2019 之前的浏览器,请不要使用MutationObserver 回调中那样的慢速 ES2015 循环,for (let v of something)除非您进行编译以使结果代码与经典for循环一样快地运行

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

  3. 如果处理很复杂和/或需要很多时间,则可能会导致绘制帧很长、无响应/卡顿,因此在这种情况下,您可以使用去抖动或类似技术,例如在外部数组中累积突变并通过以下方式安排运行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;
    }
    

回到问题:

观察一个非常确定的容器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});
@Bergi,适用于处理复杂且需要大量时间的情况。MutationObserver 在微任务队列中运行,因此多个回调可能仍驻留在一个任务中,这可能导致绘制帧很长和卡顿。
2021-03-20 04:52:06
此外,您可能想要更新列表 - 例如for... of现在在 V8 中与常规 for 循环一样快:]
2021-03-22 04:52:06
是的,在最新版本的 Chrome 中,它最终只慢了 10% 甚至更少。
2021-03-22 04:52:06
显然我不能立即奖励赏金,但我给这个答案奖励 50 分,因为我发现观看稀有元素的实时 DOM 收集技巧很聪明!好答案!
2021-03-27 04:52:06
有一个中间阶段,它被转换为常规循环(带有计数器),但显然它在github.com/v8/v8/commit /... 中被替换为它自己的 IR 指令- 非常酷的东西!在任何情况下,for...of 与 for 在这里应该无关紧要(值得一提的是,这种优化是针对数组的——因此该建议对于 NodeLists 仍然具有一些优点)
2021-03-28 04:52:06