web-dev-qa-db-ja.com

DOM要素のネイティブヒットテストの最適化(Chrome)

heavyly最適化されたJavaScriptアプリ、高度にインタラクティブなグラフエディターがあります。大量のデータ(グラフ内の数千の形状)を使用して(Chrome dev-toolsを使用して)プロファイリングを開始しましたが、以前は異常なパフォーマンスのボトルネックが発生しましたヒットテスト

| Self Time       | Total Time      | Activity            |
|-----------------|-----------------|---------------------|
| 3579 ms (67.5%) | 3579 ms (67.5%) | Rendering           |
| 3455 ms (65.2%) | 3455 ms (65.2%) |   Hit Test          | <- this one
|   78 ms  (1.5%) |   78 ms  (1.5%) |   Update Layer Tree |
|   40 ms  (0.8%) |   40 ms  (0.8%) |   Recalculate Style |
| 1343 ms (25.3%) | 1343 ms (25.3%) | Scripting           |
|  378 ms  (7.1%) |  378 ms  (7.1%) | Painting            |

これはすべての65%(!)を占め、コードベースのモンスターのボトルネックのままです。これがポインタの下のオブジェクトをトレースするプロセスであることを私は知っています、そしてこれをどのように最適化できるか(より少ない要素を使用する、より少ないマウスイベントを使用するなど)について私の役に立たない考えがあります。

コンテキスト:上記のパフォーマンスプロファイルは、アプリの「画面パン」機能を示しています。この機能では、空の領域をドラッグすることで画面のコンテンツを移動できます。 。これにより、多くのオブジェクトが移動され、各オブジェクトではなくコンテナを個別に移動することで最適化されます。 デモを作成しました。


これに飛び込む前に、ヒットテストを最適化する一般原則(古き良きもの"No sh * t、Sherlock"ブログ記事)も検索したいと思いました。この目的でパフォーマンスを向上させるためのトリックが存在するかのように(translate3dを使用してGPU処理を有効にするなど)。

jsoptimize hit test のようなクエリを試しましたが、結果はグラフィックプログラミングの記事と手動の実装例でいっぱいです-まるでJSコミュニティが聞いたの前にこのこと! chrome devtools guide でさえこの領域が欠けています。

だからここで私は誇らしげに私の研究を終えました:JavaScriptでネイティブヒットテストを最適化するにはどうすればよいですか?


デモを用意しました パフォーマンスのボトルネックを示していますが、実際のアプリと同じではありません正確に数値はデバイスによっても明らかに異なります。ボトルネックを確認するには:

  1. Chrome(またはブラウザに相当するもの)の[タイムライン]タブに移動します
  2. 録音を開始し、狂人のようにデモをパンします
  3. 記録を停止し、結果を確認します

この分野ですでに行ったすべての重要な最適化の要約:

  • 何千もの要素を個別に移動するのではなく、画面上で単一のコンテナを移動する
  • transform: translate3dを使用してコンテナを移動する
  • v-マウスの動きを画面のリフレッシュレートに同期する
  • 不要な「ラッパー」要素と「フィクサー」要素をすべて削除する
  • 形状にpointer-events: noneを使用-効果なし

その他の注意事項:

  • ボトルネックはwithwithout GPUアクセラレーションの両方に存在します
  • テストは最新のChromeでのみ行われました
  • dOMはReactJSを使用してレンダリングされますが、リンクされたデモに見られるように、DOMがなくても同じ問題が観察されます。
38
John Weisz

興味深いことに、そのpointer-events: none効果はありません。しかし、考えてみると、そのフラグが設定された要素は他の要素のポインターイベントを覆い隠しているため、とにかくヒットテストを実行する必要があるため、それは理にかなっています。

あなたができることは、重要なコンテンツの上にオーバーレイを置き、そのオーバーレイ上のマウスイベントに応答し、コードにそれをどうするかを決定させることです。

これが機能するのは、ヒットテストアルゴリズムがヒットを検出すると、それがzインデックスを下向きに検出すると想定しているため、停止します。


オーバーレイ付き

// ================================================
// Increase or decrease this value for testing:
var NUMBER_OF_OBJECTS = 40000;
// Wether to use the overlay or the container directly
var USE_OVERLAY = true;
// ================================================

var overlay = document.getElementById("overlay");
var container = document.getElementById("container");
var contents = document.getElementById("contents");

for (var i = 0; i < NUMBER_OF_OBJECTS; i++) {
    var node = document.createElement("div");
    node.innerHtml = i;
    node.className = "node";
    node.style.top = Math.abs(Math.random() * 2000) + "px";
    node.style.left = Math.abs(Math.random() * 2000) + "px";
    contents.appendChild(node);
}

var posX = 100;
var posY = 100;
var previousX = null;
var previousY = null;

var mousedownHandler = function (e) {
    window.onmousemove = globalMousemoveHandler;
    window.onmouseup = globalMouseupHandler;
    previousX = e.clientX;
    previousY = e.clientY;
}

var globalMousemoveHandler = function (e) {
    posX += e.clientX - previousX;
    posY += e.clientY - previousY;
    previousX = e.clientX;
    previousY = e.clientY;
    contents.style.transform = "translate3d(" + posX + "px, " + posY + "px, 0)";
}

var globalMouseupHandler = function (e) {
    window.onmousemove = null;
    window.onmouseup = null;
    previousX = null;
    previousY = null;
}

if(USE_OVERLAY){
        overlay.onmousedown = mousedownHandler;
}else{
        overlay.style.display = 'none';
        container.onmousedown = mousedownHandler;
}


contents.style.transform = "translate3d(" + posX + "px, " + posY + "px, 0)";
#overlay{
  position: absolute;
  top: 0;
  left: 0;
  height: 400px;
  width: 800px;
  opacity: 0;
  z-index: 100;
  cursor: -webkit-grab;
  cursor: -moz-grab;
  cursor: grab;
  -moz-user-select: none;
  -ms-user-select: none;
  -webkit-user-select: none;
  user-select: none;
}

#container {
  height: 400px;
  width: 800px;
  background-color: #ccc;
  overflow: hidden;
}

#container:active {
  cursor: move;
  cursor: -webkit-grabbing;
  cursor: -moz-grabbing;
  cursor: grabbing;
}

.node {
  position: absolute;
  height: 20px;
  width: 20px;
  background-color: red;
  border-radius: 10px;
  pointer-events: none;
}
<div id="overlay"></div>
<div id="container">
    <div id="contents"></div>
</div>

オーバーレイなし

// ================================================
// Increase or decrease this value for testing:
var NUMBER_OF_OBJECTS = 40000;
// Wether to use the overlay or the container directly
var USE_OVERLAY = false;
// ================================================

var overlay = document.getElementById("overlay");
var container = document.getElementById("container");
var contents = document.getElementById("contents");

for (var i = 0; i < NUMBER_OF_OBJECTS; i++) {
    var node = document.createElement("div");
    node.innerHtml = i;
    node.className = "node";
    node.style.top = Math.abs(Math.random() * 2000) + "px";
    node.style.left = Math.abs(Math.random() * 2000) + "px";
    contents.appendChild(node);
}

var posX = 100;
var posY = 100;
var previousX = null;
var previousY = null;

var mousedownHandler = function (e) {
    window.onmousemove = globalMousemoveHandler;
    window.onmouseup = globalMouseupHandler;
    previousX = e.clientX;
    previousY = e.clientY;
}

var globalMousemoveHandler = function (e) {
    posX += e.clientX - previousX;
    posY += e.clientY - previousY;
    previousX = e.clientX;
    previousY = e.clientY;
    contents.style.transform = "translate3d(" + posX + "px, " + posY + "px, 0)";
}

var globalMouseupHandler = function (e) {
    window.onmousemove = null;
    window.onmouseup = null;
    previousX = null;
    previousY = null;
}

if(USE_OVERLAY){
        overlay.onmousedown = mousedownHandler;
}else{
        overlay.style.display = 'none';
        container.onmousedown = mousedownHandler;
}


contents.style.transform = "translate3d(" + posX + "px, " + posY + "px, 0)";
#overlay{
  position: absolute;
  top: 0;
  left: 0;
  height: 400px;
  width: 800px;
  opacity: 0;
  z-index: 100;
  cursor: -webkit-grab;
  cursor: -moz-grab;
  cursor: grab;
  -moz-user-select: none;
  -ms-user-select: none;
  -webkit-user-select: none;
  user-select: none;
}

#container {
  height: 400px;
  width: 800px;
  background-color: #ccc;
  overflow: hidden;
}

#container:active {
  cursor: move;
  cursor: -webkit-grabbing;
  cursor: -moz-grabbing;
  cursor: grabbing;
}

.node {
  position: absolute;
  height: 20px;
  width: 20px;
  background-color: red;
  border-radius: 10px;
  pointer-events: none;
}
<div id="overlay"></div>
<div id="container">
    <div id="contents"></div>
</div>
11
Manuel Otto

問題の1つは、コンテナ内のすべての要素を移動していることです。GPUアクセラレーションがあるかどうかは関係ありません。ボトルネックは、新しい位置、つまりプロセッサフ​​ィールドを再計算しています。

ここでの私の提案は、コンテナをセグメント化することです。したがって、さまざまなペインを個別に移動して負荷を軽減できます。これは、ブロードフェーズ計算と呼ばれます。つまり、移動する必要があるものだけを移動します。画面から何かが出た場合、なぜそれを移動する必要がありますか?

1つの16個のコンテナの代わりに作成することから始めます。ここでいくつかの計算を行って、これらのペインのどれが表示されているかを確認する必要があります。次に、マウスイベントが発生したときに、それらのペインのみを移動し、表示されていないペインはそのままにしておきます。これにより、移動にかかる時間が大幅に短縮されます。

+------+------+------+------+
|    SS|SS    |      |      |
|    SS|SS    |      |      |
+------+------+------+------+
|      |      |      |      |
|      |      |      |      |
+------+------+------+------+
|      |      |      |      |
|      |      |      |      |
+------+------+------+------+
|      |      |      |      |
|      |      |      |      |
+------+------+------+------+

この例では、16個のペインがあり、そのうち2個が表示されています(画面を表すSでマークされています)。ユーザーがパンするときは、「画面」の境界ボックスをチェックし、「画面」に関連するペインを見つけて、それらのペインのみを移動します。これは理論的には無限にスケーラブルです。

残念ながら、考えを示すコードを書く時間がありませんが、これがお役に立てば幸いです。

乾杯!

6