MutationObserver 在整个 DOM 中检测节点的性能
Posted
技术标签:
【中文标题】MutationObserver 在整个 DOM 中检测节点的性能【英文标题】:Performance of MutationObserver to detect nodes in entire DOM 【发布时间】:2015-10-18 00:30:00 【问题描述】:我有兴趣使用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 正文中注意<li>
,这会不会有问题:
var container = document.querySelector('body');
我知道它适用于我为自己设置的基本示例...但是不建议这样做吗?这会导致性能不佳吗?如果是这样,我将如何检测和衡量该性能问题?
我想也许有一个原因,所有 MutationObserver
示例都针对其目标容器如此具体...但我不确定。
【问题讨论】:
性能问题特定于给定场景。如果您的整个文档只有几个简单的元素,我敢肯定您不会有任何问题。如果您担心遇到性能问题,请配置文件! 我使用了大量的MutationObserver
s 并让它们递归地查看整个 DOM。我个人从来没有遇到过性能问题。
引入 MutationObservers 和弃用 MutationEvents 的主要原因是 MutationObservers 更快,因为它们将更改聚合在一起。我们还在大型文档上使用带有 subtree: true
的 MutationObservers,这从来都不是问题。
为什么要观察属性和字符数据的变化?你自己这么说——你想观察li
元素的可能添加吗?如果某事是次优性能的候选者,我会说要求比你需要的更多的事件,就是这样。
例如,Web 性能监控库 Boomerang.js (github.com/akamai/boomerang) 在整个文档上使用MutationObserver
来测量 SPA 页面加载时间。
【参考方案1】:
这个答案主要适用于大而复杂的页面。
如果在页面加载/渲染之前附加,如果页面又大又复杂(1、2),未优化的 MutationObserver 回调可能会增加页面加载时间(例如,5 秒到 7 秒)。回调作为阻止进一步处理 DOM 的微任务执行,并且可以在复杂页面上每秒触发数百或数千次。大多数示例和现有库都没有考虑到此类场景,并提供了美观、易于使用但可能很慢的 JS 代码。
始终使用devtools profiler 并尝试使您的观察者回调消耗的 CPU 时间少于页面加载期间消耗的总 CPU 时间的 1%。
通过访问 offsetTop 和类似属性来避免触发forced synchronous layout
避免使用复杂的 DOM 框架/库,如 jQuery,更喜欢原生 DOM 的东西
观察属性时,使用.observe()
中的attributeFilter: ['attr1', 'attr2']
选项。
尽可能以非递归方式观察直接父母 (subtree: false
)。
例如,通过递归观察document
等待父元素是有意义的,成功时断开观察者,在此容器元素上附加一个新的非递归元素。
当只等待一个具有id
属性的元素时,使用非常快的getElementById
而不是枚举mutations
数组(它可能有数千个条目):example .
如果页面上所需的元素相对较少(例如iframe
或object
),请使用getElementsByTagName
和getElementsByClassName
返回的实时HTMLCollection 并重新检查它们所有而不是例如,如果 mutations
有超过 100 个元素,则枚举它。
避免使用querySelector
,尤其是速度极慢的querySelectorAll
。
如果 querySelectorAll
在 MutationObserver 回调中绝对无法避免,首先执行 querySelector
检查,如果成功,则继续 querySelectorAll
。平均而言,这样的组合会快很多。
如果针对 2018 之前的 Chrome/ium,请不要使用需要回调的内置 Array 方法,例如 forEach、filter 等,因为在 Chrome 的 V8 中,与经典的 @987654345 相比,调用这些函数的成本一直很高@loop(慢 10-100 倍),而 MutationObserver 回调可能会报告复杂现代页面上的数千个节点。
如果针对 2019 年之前的浏览器,请不要在 MutationObserver 回调中使用 the slow ES2015 loops 之类的 for (let v of something)
,除非您进行转换,以便生成的代码与经典的 for
循环一样快。
如果目标是改变页面的外观,并且您有一种可靠且快速的方法来判断要添加的元素在页面的可见部分之外,请断开观察者的连接并通过setTimeout(fn, 0)
安排整个页面重新检查和重新处理:它将在解析/布局活动的初始爆发完成并且引擎可以“呼吸”时执行,这可能需要一秒钟。然后,您可以使用 requestAnimationFrame 以不显眼的方式处理页面。
如果处理很复杂和/或花费大量时间,则可能会导致绘制帧过长、无响应/卡顿,因此在这种情况下,您可以使用 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;
回到问题:
观察一个非常确定的容器
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);
【讨论】:
显然我不能立即奖励赏金,但我给这个答案一个 50 分的赏金,因为我发现观看稀有元素的实时 DOM 收集技巧很聪明!好答案! 另外,您可能想要更新列表 - 例如,for... of
现在与 V8 中的常规 for 循环一样快:]
是的,在最近的 Chrome 版本中,它终于慢了 10% 甚至更少。
有一个中间阶段,它被转换为一个常规循环(带有一个计数器),但显然它在github.com/v8/v8/commit/… 中被替换为它自己的 IR 指令 - 非常酷的东西!在任何情况下,for...of 与 for 在这里都无关紧要(值得一提的是,这种优化是针对数组的——因此该建议仍然对 NodeLists 有一些好处)
@Bergi,它适用于处理复杂且需要大量时间的情况。 MutationObserver 在微任务队列中运行,因此多个回调可能仍驻留在一个任务中,这可能导致很长的绘制帧和卡顿。以上是关于MutationObserver 在整个 DOM 中检测节点的性能的主要内容,如果未能解决你的问题,请参考以下文章