Pregunta Optimización de pruebas de detección nativas de elementos DOM (Chrome)


tengo un fuertemente aplicación optimizada de JavaScript, un editor de gráficos altamente interactivo. Ahora comencé a perfilarlo (usando las herramientas de desarrollo de Chrome) con grandes cantidades de datos (miles de formas en el gráfico), y me encuentro con un cuello de botella de rendimiento inusual anteriormente, 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            |

Esto toma 65% de todo (!), siendo un cuello de botella de monstruo en mi base de código. Sé que este es el proceso de rastrear el objeto debajo del puntero, y tengo mis inútiles ideas sobre cómo podría optimizarse (usar menos elementos, usar menos eventos de mouse, etc.).

Contexto: El perfil de rendimiento anterior muestra una función de "panorámica de pantalla" en mi aplicación, donde se puede mover el contenido de la pantalla arrastrando el área vacía. Esto da como resultado que se muevan muchos objetos, optimizados moviendo su contenedor en lugar de cada objeto individualmente. Hice una demostración.


Antes de saltar a esto, quería buscar el principios generales de optimizar las pruebas de impacto (esos buenos ol ' "No sh * t, Sherlock" artículos de blog), así como si existen trucos para mejorar el rendimiento en este extremo (como el uso de translate3d para habilitar el procesamiento de GPU).

Intenté hacer consultas como js optimize hit test, pero los resultados están llenos de artículos de programación de gráficos y ejemplos de implementación manual: es como si la comunidad de JS no tuviera ni siquiera oído de esto antes! Incluso el cromo guía de devtools carece de esta área

Así que aquí estoy, orgullosamente hecho con mi investigación, preguntando: ¿Cómo puedo optimizar las pruebas de impacto nativas en JavaScript?


Preparé una demostración eso demuestra el cuello de botella de rendimiento, aunque no es exactamente lo mismo que mi aplicación real, y los números obviamente también variarán según el dispositivo. Para ver el cuello de botella:

  1. Ve a la pestaña Línea de tiempo en Chrome (o el equivalente de tu navegador)
  2. Comience a grabar, luego recorra la demo como un loco
  3. Deje de grabar y verifique los resultados

Un resumen de todas las optimizaciones significativas que ya he hecho en esta área:

  • mover un solo contenedor en la pantalla en lugar de mover miles de elementos individualmente
  • utilizando transform: translate3d mover el contenedor
  • Sincronización v del movimiento del mouse a la frecuencia de actualización de la pantalla
  • eliminando todos los posibles elementos innecesarios de "envoltura" y "fijador"
  • utilizando pointer-events: none en formas - sin efecto

Notas adicionales:

  • el cuello de botella existe con y sin Aceleración GPU
  • las pruebas solo se realizaron en Chrome, la última
  • el DOM se procesa utilizando ReactJS, pero el mismo problema es observable sin él, como se ve en la demostración vinculada

32
2018-01-24 14:19


origen


Respuestas:


Interesante, eso pointer-events: none no tiene efecto. Pero si lo piensas bien, tiene sentido, ya que los elementos con esa bandera todavía ocultan los eventos de puntero de otros elementos, por lo que la prueba de éxito debe llevarse a cabo de todos modos.

Lo que puede hacer es colocar una superposición sobre el contenido crítico y responder a los eventos del mouse en esa superposición, deje que su código decida qué hacer con él.

Esto funciona porque una vez que el algoritmo de hittest ha encontrado un hit, y supongo que lo hace hacia abajo, el índice z se detiene.


Con superposición

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

Sin superposición

// ================================================
// 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
2017-07-26 08:44



Uno de los problemas es que está moviendo CADA elemento individual dentro de su contenedor, no importa si tiene aceleración de GPU o no, el cuello de la botella está recalculando su nueva posición, es decir, el campo del procesador.

Mi sugerencia aquí es segmentar los contenedores, por lo tanto, puede mover varios paneles de forma individual, reduciendo la carga, esto se denomina cálculo de fase amplia, es decir, solo mover lo que se debe mover. Si sacó algo de la pantalla, ¿por qué debería moverlo?

Comience por hacer en lugar de uno, 16 contenedores, tendrá que hacer algunos cálculos aquí para descubrir cuál de estos paneles se muestra. Luego, cuando ocurre un evento de mouse, mueve solo esos paneles y deja los que no se muestran donde están. Esto debería reducir en gran medida el tiempo utilizado para moverlos.

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

En este ejemplo, tenemos 16 paneles, de los cuales, 2 se muestran (marcados con S para Pantalla). Cuando un usuario hace una búsqueda, verifique el cuadro delimitador de la "pantalla", descubra qué paneles pertenecen a la "pantalla", mueva solo esos paneles. Esto es teóricamente infinitamente escalable.

Lamentablemente, me falta tiempo para escribir el código que muestra el pensamiento, pero espero que esto te ayude.

¡Aclamaciones!


5
2018-02-03 12:51