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でネイティブヒットテストを最適化するにはどうすればよいですか?
デモを用意しました パフォーマンスのボトルネックを示していますが、実際のアプリと同じではありません正確に数値はデバイスによっても明らかに異なります。ボトルネックを確認するには:
この分野ですでに行ったすべての重要な最適化の要約:
transform: translate3d
を使用してコンテナを移動するpointer-events: none
を使用-効果なしその他の注意事項:
興味深いことに、その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>
問題の1つは、コンテナ内のすべての要素を移動していることです。GPUアクセラレーションがあるかどうかは関係ありません。ボトルネックは、新しい位置、つまりプロセッサフィールドを再計算しています。
ここでの私の提案は、コンテナをセグメント化することです。したがって、さまざまなペインを個別に移動して負荷を軽減できます。これは、ブロードフェーズ計算と呼ばれます。つまり、移動する必要があるものだけを移動します。画面から何かが出た場合、なぜそれを移動する必要がありますか?
1つの16個のコンテナの代わりに作成することから始めます。ここでいくつかの計算を行って、これらのペインのどれが表示されているかを確認する必要があります。次に、マウスイベントが発生したときに、それらのペインのみを移動し、表示されていないペインはそのままにしておきます。これにより、移動にかかる時間が大幅に短縮されます。
+------+------+------+------+
| SS|SS | | |
| SS|SS | | |
+------+------+------+------+
| | | | |
| | | | |
+------+------+------+------+
| | | | |
| | | | |
+------+------+------+------+
| | | | |
| | | | |
+------+------+------+------+
この例では、16個のペインがあり、そのうち2個が表示されています(画面を表すSでマークされています)。ユーザーがパンするときは、「画面」の境界ボックスをチェックし、「画面」に関連するペインを見つけて、それらのペインのみを移動します。これは理論的には無限にスケーラブルです。
残念ながら、考えを示すコードを書く時間がありませんが、これがお役に立てば幸いです。
乾杯!