javascript - Prestanda för MutationObserver att upptäcka noder i hela DOM


Jag är intresserad av att använda MutationObserver för att upptäcka om ett visst HTML-element läggs till någonstans på en HTML-sida. Som exempel säger jag att jag vill upptäcka om några <li>-element läggs till någonstans i DOM:en.

Alla MutationObserver-exempel jag har sett hittills upptäcker bara om en nod läggs till i en viss behållare. Till exempel:

någon HTML

<body>

  ...

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

  ...

</body>

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

Så i detta exempel är MutationObserver uppsatt för att bevaka en mycket specifik behållare (ul#my-list) för att se om några <li>-element läggs till den.

Är det ett problem om jag ville vara mindre specifik och leta efter <li>-taggar över hela HTML-dokumentet på detta sätt:

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

Jag vet att det fungerar i de grundläggande exempel jag har satt upp för mig själv... Men är det inte rekommenderat att göra detta? Kommer detta att leda till dålig prestanda? Och i så fall, hur skulle jag upptäcka och mäta det prestandaproblemet?

Jag tänkte att det kanske fanns en anledning till att alla MutationObserver-exempel är så specifika med sin riktade behållare... men jag är inte säker.

Svar #1

Detta svar gäller främst stora och komplexa sidor.

Om det är fäst innan sidladdning/rendering kan en icke-optimerad MutationObserver-callback lägga till några sekunder till sidladdningstiden (säg 5 sekunder till 7 sekunder) om sidan är stor och komplex (1, 2). Callbacken körs som en mikrouppgift som blockerar vidare bearbetning av DOM och kan utlösas hundratals eller tusentals gånger per sekund på en komplex sida. De flesta exempel och befintliga bibliotek tar inte hänsyn till sådana scenarier och erbjuder snygg, lättanvänd men potentiellt långsam JS-kod.

  1. Använd alltid devtools-profilern och försök få din observer-callback att konsumera mindre än 1% av den totala CPU-tiden som används under sidans laddning.

  2. Undvik att utlösa tvingad synkron layout genom att komma åt offsetTop och liknande egenskaper

  3. Undvik att använda komplexa DOM-ramverk/bibliotek som jQuery, föredra inbyggda DOM-funktioner

  4. När du observerar attribut, använd attributeFilter: ['attr1', 'attr2'] alternativ i .observe().

  5. Observera direkt föräldrar icke-rekursivt (subtree: false) när det är möjligt.
    Till exempel är det vettigt att vänta på förälderelementet genom att observera document rekursivt, koppla bort observatören vid framgång, och fäst en ny icke-rekursiv på detta containerelement.

  6. När du väntar på bara ett element med ett id-attribut, använd den galet snabba getElementById istället för att enumerera mutations-arrayen (den kan ha tusentals poster): exempel.

  7. Om det önskade elementet är relativt ovanligt på sidan (t.ex. iframe eller object) använd den live HTMLCollection som returneras av getElementsByTagName och getElementsByClassName och kontrollera dem alla igen istället för att uppräkna mutations om det till exempel finns fler än 100 element.

  8. Undvik att använda querySelector och särskilt det extremt långsamma querySelectorAll.

  9. Om querySelectorAll är absolut oundvikligt inuti en MutationObserver callback, utför först en querySelector-kontroll, och om den lyckas, fortsätt med querySelectorAll. I genomsnitt kommer en sådan kombination att vara mycket snabbare.

  10. Om du riktar in dig på Chrome/ium före 2018, använd inte de inbyggda Array-metoderna som forEach, filter, etc. som kräver callbacks eftersom dessa funktioner i Chromes V8 alltid har varit dyra att anropa jämfört med den klassiska for (var i=0 ....)-loopen (10-100 gånger långsammare), och MutationObserver callback kan rapportera tusentals noder på komplexa moderna sidor.

  • Det alternativa funktionella uppräkningen som stöds av lodash eller liknande snabb bibliotek är okej även i äldre webbläsare.
  • Från och med 2018 inlinar Chrome/ium de standardiserade inbyggda metoderna för arrayer.
  1. Om du riktar dig mot webbläsare före 2019, använd inte de långsamma ES2015-looparna som for (let v of something) inuti MutationObserver-callbacken om du inte transpilerar så att den resulterande koden körs lika snabbt som den klassiska for-loopen.

  2. Om målet är att ändra hur sidan ser ut och du har en pålitlig och snabb metod för att avgöra att element som läggs till är utanför den synliga delen av sidan, koppla bort observatören och schemalägg en fullständig omläsning och ombearbetning av sidan via setTimeout(fn, 0): det kommer att köras när den initiala vågen av parsning/layoutaktivitet är avslutad och motorn kan "andas", vilket kan ta till och med en sekund. Sedan kan du obemärkt bearbeta sidan i delar med hjälp av requestAnimationFrame, till exempel.

  3. Om bearbetningen är komplex och/eller tar mycket tid kan det leda till mycket långa målningsramar, bristande respons/jank, så i detta fall kan du använda debounce eller en liknande teknik, t.ex. ackumulera mutationer i en yttre array och schemalägga en körning via 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;
    }
    

    Observera att requestAnimationFrame endast utlöses när sidan är (eller blir) synlig.

Tillbaka till frågan:

övervaka en mycket specifik behållare ul#my-list för att se om några <li> läggs till i den.

Eftersom li är ett direkt barn, och vi letar efter tillagda noder, det enda alternativ som behövs är childList: true (se råd #2 ovan).

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

    // Stop observing if needed:
    observer.disconnect();
}).observe(document.querySelector('ul#my-list'), {childList: true});

Relaterade artiklar