1. ホーム
  2. javascript

[解決済み] DOM全体のノードを検出するMutationObserverの性能

2023-01-14 14:46:57

質問

私は MutationObserver を使って、ある HTML 要素が HTML ページのどこかに追加されたかどうかを検出することに興味があります。例として、私は任意の <li> が DOM のどこかに追加されているかどうかを検出したいとします。

すべての MutationObserver の例は、ノードが特定のコンテナに追加された場合のみ検出します。例えば

いくつかの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 の例が対象となるコンテナについて非常に具体的であるのは、そのためかもしれないと思いました......が、よくわかりません。

どのように解決するのですか?

この答えは、主に大きく、複雑なページに適用されます。

ページのロード/レンダリングの前にアタッチされた場合、最適化されていない MutationObserver コールバックは、ページが大きくて複雑な場合、ページのロード時間に数秒を追加できます(たとえば、5 秒から 7 秒) ( 1 , 2 ). コールバックはマイクロタスクとして実行され、DOM のさらなる処理をブロックし、複雑なページでは 1 秒間に数百回から数千回実行される可能性があります。ほとんどの例や既存のライブラリは、このようなシナリオを考慮しておらず、見栄えが良く、使いやすいですが、潜在的に遅い JS コードを提供しています。

  1. 常に devtools プロファイラ を使用し、オブザーバコールバックがページロード中に消費する CPU 時間全体の 1% 未満になるようにします。

  2. をトリガーしないようにしましょう。 強制的な同期レイアウト offsetTopや類似のプロパティにアクセスすることで

  3. jQueryのような複雑なDOMフレームワーク/ライブラリの使用は避け、ネイティブDOMのものを好む

  4. 属性を観察するときは attributeFilter: ['attr1', 'attr2'] オプションで .observe() .

  5. 可能な限り、非再帰的に直接の親を観察する ( subtree: false ).

    例えば、親要素を待つために、観察することは意味があります document を再帰的に観測し、成功したらオブザーバを切り離し、このコンテナ要素に新しい非再帰的なオブザーバをアタッチします。

  6. で一つの要素だけを待つときは id 属性がある場合は、非常に高速な getElementById ではなく を列挙するのではなく mutations の配列を列挙します (数千のエントリーがあるかもしれません)。 .

  7. 目的の要素がページ上で比較的少ない場合(例えば iframe または object によって返されるライブ HTMLCollection を使用します。 getElementsByTagNamegetElementsByClassName を追加し、それらをすべて再確認します。 ではなく を列挙するのではなく mutations を列挙するのではなく、例えば100以上の要素がある場合は

  8. の使用は避ける。 querySelector の使用は避け、特に極端に遅い querySelectorAll .

  9. もし querySelectorAll が MutationObserver のコールバック内で絶対に避けられない場合は、最初に querySelector をチェックし、成功したら querySelectorAll . 平均して、このようなコンボはかなり速くなります。

  10. 2018年以前のChrome/iumをターゲットにしている場合、コールバックを必要とするforEach、filterなどの組み込みのArrayメソッドを使用しないでください。 for (var i=0 ....) ループと比較して常に高価であり(10~100 倍遅い)、MutationObserver コールバックは複雑な最新ページで数千のノードを報告することがあります。

  • lodashまたは同様の高速なライブラリに支えられた代替の関数列挙は、古いブラウザでも大丈夫です。
  • 2018年現在、Chrome/iumは標準的な配列の組み込みメソッドをインライン化しています。
  1. 2019年以前のブラウザをターゲットにしている場合は は使用しないでください。 のように for (let v of something) のようなループを MutationObserver コールバックの内部で行う場合、トランスパイルしない限り、結果として得られるコードは古典的な for ループのように高速に実行されます。

  2. もし目的がページの見え方を変えることで、追加される要素がページの見える部分の外側にあることを伝える信頼できる速い方法があるならば、オブザーバを切断し、ページ全体の再チェックと再処理をスケジュールするために、次のようにします。 setTimeout(fn, 0) を介してページ全体の再チェックと再処理をスケジュールします。これは、解析/レイアウトのアクティビティの最初のバーストが終了し、エンジンが呼吸できるようになったときに実行されます。その後、たとえば requestAnimationFrame を使用して、チャンク内のページを目立たないように処理することができます。

  3. 処理が複雑であったり、多くの時間を要する場合、非常に長いペイントフレームや無反応/ジャンクにつながる可能性があるので、このような場合は デバウンス を使用するか、または同様の手法 (たとえば、外部配列に変異を蓄積し、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;
    }
    
    

質問に戻ります。

ある容器を見る 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});