Производительность MutationObserver для обнаружения узлов во всем DOM
Я заинтересован в использовании MutationObserver
определить, добавлен ли определенный элемент HTML где-либо на странице HTML. Например, я скажу, что я хочу обнаружить, если таковые имеются <li>
добавляются где угодно в DOM.
Все 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>
к нему прилагаются.
Это проблема, если я хотел быть менее конкретным, и следить за <li>
по всему телу HTML вот так:
var container = document.querySelector('body');
Я знаю, что это работает в основных примерах, которые я настроил для себя... Но разве не рекомендуется делать это? Это приведет к снижению производительности? И если да, то как бы я обнаружил и измерил эту проблему производительности?
Я подумал, может быть, была причина того, что все MutationObserver
примеры настолько специфичны с их целевым контейнером... но я не уверен.
1 ответ
Этот ответ относится к большим и сложным страницам.
Особенно, если к загрузке страницы прикреплен наблюдатель (то есть document_start
/ document-start
в расширениях Chrome /WebExtensions/userscripts или просто в обычном синхронном скрипте страницы внутри <head>
), но также и на огромных динамически обновляемых страницах, например, сравнение веток на GitHub. Неоптимизированный обратный вызов MutationObserver может добавить несколько секунд к времени загрузки страницы, если страница большая и сложная ( 1, 2). Большинство примеров и существующих библиотек не учитывают такие сценарии и предлагают красивый, простой в использовании, но медленный код js.
Обратный вызов MutationObserver выполняется в виде микрозадачи, которая блокирует дальнейшую обработку DOM и может запускаться сотни или тысячи раз в секунду на сложной странице.
Всегда используйте профилировщик devtools и старайтесь, чтобы ваш обратный вызов наблюдателя занимал менее 1% общего времени ЦП, расходуемого во время загрузки страницы.
Избегайте принудительного запуска синхронного макета путем доступа к offsetTop и аналогичным свойствам
Избегайте использования сложных структур / библиотек DOM, таких как jQuery, предпочитайте нативный DOM
При наблюдении атрибутов используйте
attributeFilter: ['attr1', 'attr2']
вариант в.observe()
,По возможности наблюдать за прямыми родителями нерекурсивно (
subtree: false
).
Например, имеет смысл ждать родительский элемент, наблюдаяdocument
рекурсивно отключить наблюдателя в случае успеха, прикрепить новый нерекурсивный элемент к этому элементу контейнера.При ожидании только одного элемента с
id
атрибут, используйте безумно быстроgetElementById
вместо того, чтобы перечислятьmutations
массив (может иметь тысячи записей): пример.В случае, если нужный элемент является относительно редким на странице (например,
iframe
или жеobject
) использовать живой HTMLCollection, возвращенныйgetElementsByTagName
а такжеgetElementsByClassName
и перепроверить их все вместо перечисленияmutations
например, если в нем более 100 элементов.Избегать использования
querySelector
и особенно крайне медленноquerySelectorAll
,Если
querySelectorAll
абсолютно неизбежен внутри обратного вызова MutationObserver, сначала выполнитеquerySelector
проверить, и в случае успеха продолжитьquerySelectorAll
, В среднем такая комбинация будет намного быстрее.Если вы нацелены на браузеры с несмываемым краем, не используйте встроенные методы Array, такие как 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 будут встроены стандартные методы встроенного массива.
Если вы нацелены на браузеры без кровотечений, не используйте медленные циклы ES2015, такие как
for (let v of something)
внутри обратного вызова MutationObserver, если вы не переносите так, чтобы результирующий код работал так же быстро, как классическийfor
петля.Если цель состоит в том, чтобы изменить внешний вид страницы, и у вас есть надежный и быстрый способ сообщить, что добавляемые элементы находятся за пределами видимой части страницы, отключите наблюдателя и запланируйте повторную проверку и повторную обработку всей страницы с помощью
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});