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.
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.
Undvik att utlösa tvingad synkron layout genom att komma åt offsetTop och liknande egenskaper
Undvik att använda komplexa DOM-ramverk/bibliotek som jQuery, föredra inbyggda DOM-funktioner
När du observerar attribut, använd
attributeFilter: ['attr1', 'attr2']alternativ i.observe().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 observeradocumentrekursivt, koppla bort observatören vid framgång, och fäst en ny icke-rekursiv på detta containerelement.När du väntar på bara ett element med ett
id-attribut, använd den galet snabbagetElementByIdistället för att enumereramutations-arrayen (den kan ha tusentals poster): exempel.Om det önskade elementet är relativt ovanligt på sidan (t.ex.
iframeellerobject) använd den live HTMLCollection som returneras avgetElementsByTagNameochgetElementsByClassNameoch kontrollera dem alla igen istället för att uppräknamutationsom det till exempel finns fler än 100 element.Undvik att använda
querySelectoroch särskilt det extremt långsammaquerySelectorAll.Om
querySelectorAllär absolut oundvikligt inuti en MutationObserver callback, utför först enquerySelector-kontroll, och om den lyckas, fortsätt medquerySelectorAll. I genomsnitt kommer en sådan kombination att vara mycket snabbare.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.
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 klassiskafor-loopen.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.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-listfö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});
