web-dev-qa-db-fra.com

Performances de MutationObserver pour détecter les nœuds dans tout le DOM

Je suis intéressé par l'utilisation de MutationObserver pour détecter si un certain élément HTML est ajouté n'importe où dans une page HTML. Par exemple, je dirai que je veux détecter s'il y a <li> sont ajoutés n'importe où dans le DOM.

Tous les exemples MutationObserver que j'ai vus jusqu'à présent ne détectent que si un nœud est ajouté à un conteneur particulier. Par exemple:

du HTML

<body>

  ...

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

  ...

</body>

MutationObserver définition

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

Ainsi, dans cet exemple, le MutationObserver est configuré pour surveiller un certain conteneur (ul#my-list) pour voir s'il y en a <li> y sont ajoutés.

Est-ce un problème si je voulais être moins spécifique, et surveiller <li> sur tout le corps HTML comme ceci:

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

Je sais que cela fonctionne dans les exemples de base que j'ai configurés pour moi-même ... Mais n'est-il pas conseillé de le faire? Cela entraînera-t-il de mauvaises performances? Et si oui, comment pourrais-je détecter et mesurer ce problème de performances?

J'ai pensé qu'il y avait peut-être une raison pour laquelle tous les exemples de MutationObserver sont si spécifiques avec leur conteneur ciblé ... mais je ne suis pas sûr.

55
Jake Wilson

Cette réponse s'applique aux pages volumineuses et complexes.

Surtout si un observateur est attaché avant le début du chargement de la page (c'est-à-dire document_start/document-start Dans Chrome extensions/WebExtensions/userscripts ou simplement dans un script de page synchrone normal) à l'intérieur de <head>), mais aussi sur de gigantesques pages mises à jour dynamiquement, par exemple la comparaison de branche sur GitHub. Un rappel de MutationObserver non optimisé peut ajouter quelques secondes au temps de chargement de la page si la page est grande et complexe ( 1 =, 2 ). La plupart des exemples et des bibliothèques existantes ne tiennent pas compte de ces scénarios et offrent un code js beau, facile à utiliser mais lent.

Le rappel MutationObserver est exécuté comme une microtâche qui bloque le traitement ultérieur du DOM et peut être déclenché des centaines ou des milliers de fois par seconde sur une page complexe.

  1. Utilisez toujours le devtools profiler et essayez de faire en sorte que le rappel de votre observateur consomme moins de 1% du temps CPU total consommé pendant le chargement de la page.

  2. Évitez le déclenchement disposition synchrone forcée en accédant à offsetTop et à des propriétés similaires

  3. Évitez d'utiliser des cadres/bibliothèques DOM complexes comme jQuery, préférez les éléments DOM natifs

  4. Lors de l'observation des attributs, utilisez l'option attributeFilter: ['attr1', 'attr2'] Dans .observe().

  5. Dans la mesure du possible, observez les parents directs de manière non récursive (subtree: false).
    Par exemple, il est logique d'attendre l'élément parent en observant document récursivement, déconnecter l'observateur en cas de succès, attacher un nouveau élément non récursif sur cet élément conteneur.

  6. Lorsque vous attendez un seul élément avec un attribut id, utilisez le getElementById incroyablement rapide au lieu d'énumérer le mutations array (il peut contenir des milliers d'entrées): exemple .

  7. Dans le cas où l'élément souhaité est relativement rare sur la page (par exemple iframe ou object), utilisez la HTMLCollection en direct retournée par getElementsByTagName et getElementsByClassName et revérifiez-les toutes au lieu d'énumérer le mutations s'il a plus de 100 éléments, par exemple.

  8. Évitez d'utiliser querySelector et surtout le querySelectorAll extrêmement lent.

  9. Si querySelectorAll est absolument inévitable dans le rappel MutationObserver, effectuez d'abord une vérification de querySelector, et en cas de succès, passez à querySelectorAll. En moyenne, un tel combo sera beaucoup plus rapide.

  10. Si vous ciblez des navigateurs Edge qui ne saignent pas, n'utilisez pas de méthodes de tableau intégrées comme forEach, filter, etc. qui nécessitent des rappels car dans Chrome V8, ces fonctions ont toujours été coûteuses à invoquer par rapport à la fonction classique for (var i=0 ....) boucle (10-100 fois plus lente, mais l'équipe V8 y travaille [2017]), et le rappel MutationObserver peut se déclencher 100 fois par seconde avec des dizaines, des centaines ou des milliers de addedNodes dans chaque lot de mutations sur un complexe moderne pages.

    L'intégration de tableaux intégrés n'est pas universelle, elle se produit généralement dans un code primitif de type benchmark. Dans le monde réel, MutationObserver a des pics d'activité intermittents (comme 1 à 1 000 nœuds rapportés 100 fois par seconde) et les rappels ne sont jamais aussi simples que return x * x Donc le code n'est pas détecté comme suffisamment "chaud" pour être aligné/optimisé.

    L'énumération fonctionnelle alternative soutenue par lodash ou une bibliothèque rapide similaire est cependant correcte. À partir de 2018 Chrome et le V8 sous-jacent aligneront les méthodes intégrées du tableau standard.

  11. Si vous ciblez des navigateurs Edge qui ne saignent pas, n'utilisez pas les boucles lentes ES2015 comme for (let v of something) dans le rappel MutationObserver à moins que vous ne transposiez pour que le code résultant s'exécute aussi vite que le _ for boucle.

  12. Si l'objectif est de modifier l'apparence de la page et que vous disposez d'une méthode fiable et rapide pour dire que les éléments ajoutés sont en dehors de la partie visible de la page, déconnectez l'observateur et planifiez une nouvelle vérification et un nouveau traitement de la page via setTimeout(fn, 0) : il sera exécuté lorsque la rafale initiale d'activité d'analyse/de mise en page est terminée et que le moteur peut "respirer", ce qui pourrait prendre même une seconde. Ensuite, vous pouvez traiter discrètement la page en morceaux à l'aide de requestAnimationFrame, par exemple.

Retour à la question:

regardez un conteneur très certain ul#my-list pour voir si un <li> y est ajouté.

Puisque li est un enfant direct et que nous recherchons des nœuds ajoutés, la seule option nécessaire est childList: true (Voir conseil n ° 2 ci-dessus).

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

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