Optimiser les tests de succès natifs des éléments DOM (Chrome)

j'ai une heavily application JavaScript optimisée, un éditeur de graphiques hautement interactif. J'ai maintenant commencé à le profiler (à L'aide de Chrome dev-tools) avec des quantités massives de données (des milliers de formes dans le graphique), et je rencontre un goulot d'étranglement de performance jusque-là inhabituel, Hit Test .

| 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            |

Cela prend 65% de tout (!) , demeurant un goulot d'étranglement monstre dans ma base de codes. Je sais c'est le processus de tracer l'objet sous le pointeur , et j'ai mes idées inutiles sur la façon dont cela pourrait être optimisé (utiliser moins d'éléments, utiliser moins d'événements de souris, etc.).

contexte: le profil de performance ci-dessus montre une fonctionnalité" screen panoramique " dans mon application, où le contenu de l'écran peut être déplacé en faisant glisser la zone vide. Il en résulte beaucoup d'objets déplacés, optimisé en déplaçant leur conteneur au lieu de chaque objet individuellement. j'ai fait une démo.


avant de sauter dans ce, je voulais rechercher les principes généraux de l'optimisation des tests de frappe (ces bons vieux "No sh*t, Sherlock" articles de blog), ainsi que si des trucs existent pour améliorer les performances à cette fin (comme l'utilisation de translate3d pour activer GPU traitement.)

j'ai essayé des requêtes comme js optimize hit test , mais les résultats sont pleins d'articles de programmation graphique et d'exemples d'implémentation manuelle -- c'est comme si la communauté JS n'avait même pas entendu de cette chose avant! Même Le chrome devtools guide manque de cette zone.

alors je suis ici, fièrement fait avec mes recherches, demandant: Comment puis-je obtenir sur l'optimisation native hit testing en JavaScript?


j'ai préparé une démo qui montre le goulot d'étranglement de performance, bien que ce ne soit pas exactement la même que mon application réelle, et les numéros seront évidemment varier selon l'appareil. Pour voir le goulot d'étranglement:

  1. allez à L'onglet Timeline sur Chrome (ou l'équivalent de votre navigateur)
  2. Commencer l'enregistrement, puis un panoramique dans la démo comme un fou-homme
  3. Arrêter l'enregistrement et vérifier les résultats

un résumé de toutes les optimisations importantes que j'ai déjà faites dans ce domaine:

  • déplacement d'un seul conteneur sur l'écran au lieu de déplacer des milliers d'éléments individuellement
  • utilisant transform: translate3d pour déplacer le conteneur
  • v-la synchronisation des mouvements de la souris à l'écran de la fréquence de rafraîchissement
  • suppression de tous les éléments inutiles "d'emballage" et "de fixation
  • utilisant pointer-events: none sur les formes -- sans effet

Notes complémentaires:

  • le goulot d'étranglement existe à la fois avec et sans accélération GPU
  • les tests n'ont été effectués qu'en Chrome, dernier
  • le DOM est rendu en utilisant ReactJS, mais le même problème est observable sans lui, comme vu dans la démo liée
32
demandé sur John Weisz 2017-01-24 17:19:52

2 réponses

intéressant, que pointer-events: none n'a pas d'effet. Mais si vous y pensez, cela a du sens, puisque les éléments avec ce drapeau obscurcissent encore les événements de pointeur des autres éléments, donc le plus grand succès doit avoir lieu de toute façon.

ce que vous pouvez faire est de placer une superposition sur le contenu critique et de répondre aux événements de la souris sur cette superposition, laissez votre code décider ce qu'il doit en faire.

cela fonctionne parce qu'une fois que l'algorithme hittest a trouvé un hit, et je suis en supposant qu'il le fasse vers le bas de l'indice z, il s'arrête.


avec recouvrement

// ================================================
// 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>

sans superposition

// ================================================
// 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>
6
répondu Manuel Otto 2018-07-26 08:44:12

L'un des problèmes est que vous déplacez chaque élément à l'intérieur de votre conteneur, il n'a pas d'importance si vous avez GPU-accélération ou non, le goulot de la bouteille est de recalculer leur nouvelle position, c'est-à-dire le champ du processeur.

ma suggestion ici est de segmenter les conteneurs, donc vous pouvez déplacer divers panneaux individuellement, en réduisant la charge, cela s'appelle un calcul de large phase, c'est-à-dire, seulement déplacer ce qui doit être déplacé. Si vous avez quelque chose hors de l' screen, pourquoi le déplacer?

commencer par faire au lieu d'un, 16 conteneurs, vous aurez à faire quelques calculs ici pour savoir lequel de ces panneaux sont affichés. Ensuite, quand un événement de souris se produit, déplacez seulement ces vitres et laissez ceux qui ne sont pas affichés où ils sont. Cela devrait réduire considérablement le temps utilisé pour les déplacer.

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

dans cet exemple, nous avons 16 panneaux, dont 2 sont affichés (marqués par S pour L'écran). Lorsqu'un utilisateur casseroles, cochez la case limite de l ' "écran", déterminez quels panneaux se rapportent à l ' "écran", déplacez seulement ces panneaux. C'est théoriquement infiniment évolutive.

malheureusement je manque de temps pour écrire le code montrant la pensée, mais j'espère que cela vous aide.

santé!

5
répondu Vinícius Negrão 2017-02-03 12:51:26