Производительность MutationObserver для обнаружения узлов во всей DOM

Мне интересно использовать MutationObserver, чтобы определить, добавлен ли какой-либо элемент HTML в любом месте HTML-страницы. Например, саке, я скажу, что хочу определить, добавлены ли какие-либо <li> в DOM.

Все примеры MutationObserver, которые я видел до сих пор, обнаруживают только, если в конкретный контейнер добавлен node. Например:

некоторый 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 или просто на обычной синхронной странице script внутри <head>), но также и на огромных динамически обновляемые страницы, например отраслевое сравнение на GitHub. Неоптимизированный обратный вызов MutationObserver может добавить несколько секунд к времени загрузки страницы, если страница большая и сложная (1, 2). Большинство примеров и существующих библиотек не учитывают такие сценарии и предлагают красивый, простой в использовании, но медленный код js.

Обратный вызов MutationObserver выполняется как микрозадача, которая блокирует дальнейшую обработку DOM и может быть запущена сотнями или тысячами раз в секунду на сложной странице.

  • Всегда используйте devtools profiler и старайтесь, чтобы ваш обратный вызов наблюдателя потреблял меньше 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, если только вы transpile, чтобы полученный код работал так же быстро, как классический цикл 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});