Производительность 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 и может запускаться сотни или тысячи раз в секунду на сложной странице.

  1. Всегда используйте профилировщик devtools и старайтесь, чтобы ваш обратный вызов наблюдателя занимал менее 1% общего времени ЦП, расходуемого во время загрузки страницы.

  2. Избегайте принудительного запуска синхронного макета путем доступа к offsetTop и аналогичным свойствам

  3. Избегайте использования сложных структур / библиотек DOM, таких как jQuery, предпочитайте нативный DOM

  4. При наблюдении атрибутов используйте attributeFilter: ['attr1', 'attr2'] вариант в .observe(),

  5. По возможности наблюдать за прямыми родителями нерекурсивно (subtree: false).
    Например, имеет смысл ждать родительский элемент, наблюдая document рекурсивно отключить наблюдателя в случае успеха, прикрепить новый нерекурсивный элемент к этому элементу контейнера.

  6. При ожидании только одного элемента с id атрибут, используйте безумно быстро getElementById вместо того, чтобы перечислять mutations массив (может иметь тысячи записей): пример.

  7. В случае, если нужный элемент является относительно редким на странице (например, iframe или же object) использовать живой HTMLCollection, возвращенный getElementsByTagName а также getElementsByClassName и перепроверить их все вместо перечисления mutations например, если в нем более 100 элементов.

  8. Избегать использования querySelector и особенно крайне медленно querySelectorAll,

  9. Если querySelectorAll абсолютно неизбежен внутри обратного вызова MutationObserver, сначала выполните querySelector проверить, и в случае успеха продолжить querySelectorAll, В среднем такая комбинация будет намного быстрее.

  10. Если вы нацелены на браузеры с несмываемым краем, не используйте встроенные методы 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 будут встроены стандартные методы встроенного массива.

  11. Если вы нацелены на браузеры без кровотечений, не используйте медленные циклы ES2015, такие как for (let v of something) внутри обратного вызова MutationObserver, если вы не переносите так, чтобы результирующий код работал так же быстро, как классический for петля.

  12. Если цель состоит в том, чтобы изменить внешний вид страницы, и у вас есть надежный и быстрый способ сообщить, что добавляемые элементы находятся за пределами видимой части страницы, отключите наблюдателя и запланируйте повторную проверку и повторную обработку всей страницы с помощью 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});
Другие вопросы по тегам