Leistung von MutationObserver zur Erkennung von Knoten im gesamten DOM

78

Ich möchte MutationObserverfeststellen, ob ein bestimmtes HTML-Element irgendwo auf einer HTML-Seite hinzugefügt wird. Zum Beispiel möchte ich sagen, dass ich erkennen möchte, ob <li>irgendwo im DOM irgendwelche hinzugefügt wurden.

Alle MutationObserverBeispiele, die ich bisher gesehen habe, erkennen nur, ob einem bestimmten Container ein Knoten hinzugefügt wurde. Zum Beispiel:

etwas HTML

<body>

  ...

  <ul id='my-list'></ul>

  ...

</body>

MutationObserver Definition

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

In diesem Beispiel wird das MutationObserverSetup so eingerichtet, dass ein ganz bestimmter Container ( ul#my-list) überwacht wird, um festzustellen, ob irgendwelche <li>an ihn angehängt sind.

Ist es ein Problem, wenn ich weniger spezifisch sein und <li>über den gesamten HTML-Text wie folgt achten möchte:

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

Ich weiß, dass es in den grundlegenden Beispielen funktioniert, die ich für mich selbst eingerichtet habe ... Aber ist es nicht ratsam, dies zu tun? Wird dies zu einer schlechten Leistung führen? Und wenn ja, wie würde ich dieses Leistungsproblem erkennen und messen?

Ich dachte, es gibt vielleicht einen Grund, warum alle MutationObserverBeispiele mit ihrem Zielcontainer so spezifisch sind ... aber ich bin mir nicht sicher.

Jake Wilson
quelle
Leistungsprobleme sind spezifisch für ein bestimmtes Szenario. Wenn Ihr gesamtes Dokument nur wenige einfache Elemente enthält, werden Sie sicher keine Probleme haben. Wenn Sie Angst vor Leistungsproblemen haben, Profil!
Amit
8
Ich habe viele MutationObservers verwendet und sie rekursiv das gesamte DOM beobachten lassen. Ich persönlich hatte noch nie ein Problem mit der Leistung.
Luke
7
Der Hauptgrund für die Einführung von MutationObservers und die Ablehnung von MutationEvents liegt darin, dass MutationObservers viel schneller sind, weil sie Änderungen zusammenfassen. Wir verwenden MutationObservers auch subtree: truefür große Dokumente und es war nie ein Problem.
Loganfsmyth
1
Warum beobachten Sie Änderungen in Attributen und Zeichendaten? Sie sagen es selbst - Sie möchten mögliche Ergänzungen von liElementen beobachten? Wenn etwas ein Kandidat für eine nicht optimale Leistung ist, würde ich sagen, dass Sie nach mehr Ereignissen fragen, als Sie benötigen.
Amn
Beispielsweise verwendet Boomerang.js ( github.com/akamai/boomerang ), die MutationObserverBibliothek zur Überwachung der Webleistung, ein Gesamtdokument, um die Ladezeit von SPAs-Seiten zu messen.
Juli

Antworten:

178

Diese Antwort gilt hauptsächlich für große und komplexe Seiten.

Wenn ein nicht optimierter MutationObserver-Rückruf vor dem Laden / Rendern der Seite angehängt wird, kann die Ladezeit der Seite (z. B. 5 bis 7 Sekunden) um einige Sekunden verlängert werden, wenn die Seite groß und komplex ist ( 1 , 2 ). Der Rückruf wird als Mikrotask ausgeführt, der die weitere Verarbeitung von DOM blockiert und auf einer komplexen Seite hunderte oder tausendmal pro Sekunde ausgelöst werden kann. Die meisten Beispiele und vorhandenen Bibliotheken berücksichtigen solche Szenarien nicht und bieten gut aussehenden, benutzerfreundlichen, aber möglicherweise langsamen JS-Code.

  1. Verwenden Sie immer den devtools-Profiler und versuchen Sie, Ihren Beobachter-Rückruf weniger als 1% der gesamten CPU-Zeit zu verbrauchen, die beim Laden der Seite verbraucht wird.

  2. Vermeiden Sie das Auslösen eines erzwungenen synchronen Layouts, indem Sie auf offsetTop und ähnliche Eigenschaften zugreifen

  3. Vermeiden Sie die Verwendung komplexer DOM-Frameworks / -Bibliotheken wie jQuery und bevorzugen Sie native DOM-Inhalte

  4. Verwenden Sie beim Beobachten von Attributen die attributeFilter: ['attr1', 'attr2']Option in .observe().

  5. Beobachten Sie nach Möglichkeit direkte Eltern nicht rekursiv ( subtree: false).
    Zum Beispiel ist es sinnvoll, durch documentrekursives Beobachten auf das übergeordnete Element zu warten , den Beobachter bei Erfolg zu trennen und diesem Containerelement ein neues nicht rekursives Element beizufügen.

  6. Wenn Sie auf nur ein Element mit einem idAttribut warten , verwenden Sie das wahnsinnig schnelle Element , getElementById anstatt das mutationsArray aufzulisten (es kann Tausende von Einträgen enthalten): Beispiel .

  7. Falls das gewünschte Element auf der Seite relativ selten ist (z. B. iframeoder object), verwenden Sie die von getElementsByTagNameund zurückgegebene Live-HTMLCollection getElementsByClassNameund überprüfen Sie sie alle erneut, anstatt beispielsweise aufzuzählen, mutationsob sie mehr als 100 Elemente enthält.

  8. Vermeiden Sie die Verwendung querySelectorund vor allem die extrem langsamen querySelectorAll.

  9. Wenn dies querySelectorAllinnerhalb des MutationObserver-Rückrufs absolut unvermeidbar ist, führen Sie zuerst eine querySelectorÜberprüfung durch und fahren Sie bei Erfolg mit fort querySelectorAll. Im Durchschnitt wird eine solche Kombination viel schneller sein.

  10. Wenn Sie auf Chrome / ium vor 2018 abzielen, verwenden Sie nicht die integrierten Array-Methoden wie forEach, filter usw., für die Rückrufe erforderlich sind, da das Aufrufen dieser Funktionen in Chrome V8 im Vergleich zur klassischen for (var i=0 ....)Schleife (10-100) immer teuer war mal langsamer), und der Rückruf von MutationObserver kann Tausende von Knoten auf komplexen modernen Seiten melden.

  • Die alternative funktionale Aufzählung, die durch lodash oder eine ähnliche schnelle Bibliothek unterstützt wird, ist auch in älteren Browsern in Ordnung.
  • Ab 2018 integriert Chrome / ium die integrierten Standardmethoden für Arrays.
  1. Wenn Sie auf Browser vor 2019 abzielen, verwenden Sie die langsamen ES2015-Schleifen nicht wie for (let v of something)im MutationObserver-Rückruf, es sei denn, Sie transpilieren, damit der resultierende Code so schnell wie die klassische forSchleife ausgeführt wird.

  2. Wenn das Ziel darin besteht, das Erscheinungsbild der Seite zu ändern und Sie zuverlässig und schnell feststellen können, dass sich die hinzugefügten Elemente außerhalb des sichtbaren Teils der Seite befinden, trennen Sie den Beobachter vom Computer und planen Sie eine Überprüfung und Wiederaufbereitung der gesamten Seite über setTimeout(fn, 0): Sie wird ausgeführt, wenn die Der erste Ausbruch der Analyse- / Layout-Aktivität ist beendet und der Motor kann "atmen", was sogar eine Sekunde dauern kann. Dann können Sie die Seite beispielsweise mit requestAnimationFrame unauffällig in Blöcken verarbeiten.

  3. Verwenden Sie Debounce oder eine ähnliche Technik, z. B. akkumulieren Sie Mutationen in einem äußeren Array und planen Sie einen Lauf über 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;
    }
    

Zurück zur Frage:

Beobachten Sie einen bestimmten Container, ul#my-listum festzustellen, ob ein Container daran <li>angehängt ist.

Da liein direktes Kind ist, und wir suchen für zusätzliche Knoten, benötigt die einzige Option ist childList: true(Beratung # 2 siehe oben).

new MutationObserver(function(mutations, observer) {
    // Do something here

    // Stop observing if needed:
    observer.disconnect();
}).observe(document.querySelector('ul#my-list'), {childList: true});
wOxxOm
quelle
1
Anscheinend kann ich ein Kopfgeld nicht sofort vergeben, aber ich gebe dieser Antwort ein Kopfgeld von 50 Punkten, weil ich den Trick der Live-DOM-Sammlung zum Betrachten seltener Elemente klug finde! Gute Antwort!
Benjamin Gruenbaum
Vielleicht möchten Sie auch die Liste aktualisieren - zum Beispiel for... ofist sie jetzt in V8 so schnell wie eine reguläre for-Schleife:]
Benjamin Gruenbaum
Ja, in neueren Versionen von Chrome ist es schließlich nur um 10% oder sogar weniger langsamer.
wOxxOm
Es gab eine Zwischenstufe, in der es in eine reguläre Schleife (mit einem Zähler) umgewandelt wurde, aber anscheinend wurde es in github.com/v8/v8/commit/… durch eine eigene IR-Anweisung ersetzt - ziemlich cooles Zeug! In jedem Fall sollte für ... von vs für hier nicht viel von Bedeutung sein (erwähnenswert, dass diese Optimierung für Arrays gilt - der Rat hat also noch einige Vorteile für NodeLists)
Benjamin Gruenbaum