MutationObserver 检测整个节点的性能 DOM

Performance of MutationObserver to detect nodes in entire 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 body 中观察 <li> 是否有问题像这样:

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

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

我想也许所有 MutationObserver 示例都针对其目标容器如此具体是有原因的……但我不确定。

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

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

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

  2. 通过访问 offsetTop 和类似属性避免触发 forced synchronous layout

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

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

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

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

  7. 如果所需元素在页面上相对较少(例如 iframeobject),请使用 getElementsByTagName 和 [=22 返回的实时 HTMLCollection =] 并重新检查所有 而不是 枚举 mutations ,如果它有超过 100 个元素,例如。

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

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

  10. 如果目标是 2018 年之前的 Chrome/ium,请不要使用需要回调的内置数组方法,例如 forEach、过滤器等,因为在 Chrome 中与经典 for (var i=0 ....) 循环(慢 10-100 倍)相比,V8 调用这些函数的成本一直很高,并且 MutationObserver 回调可能会在复杂的现代页面上报告数千个节点。

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

  2. 如果目标是改变页面的外观,并且您有一种可靠且快速的方法来判断添加的元素在页面的可见部分之外,请断开观察者并安排整个通过 setTimeout(fn, 0) 重新检查和重新处理页面:它将在 parsing/layouting activity 的初始突发完成并且引擎可以“呼吸”时执行,这可能需要一秒钟。然后,您可以使用 requestAnimationFrame 等不显眼地分块处理页面。

  3. 如果处理复杂and/or需要很多时间,可能会导致画框很长,unresponsiveness/jank,所以在这种情况下可以使用debounce 或类似的技术,例如在外部数组中累积突变并通过 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;
    }
    

    请注意,requestAnimationFrame 仅在页面可见(或变得可见)时触发。

回到问题:

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

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