Оптимизация тестирования удаленных элементов DOM (Chrome)

У меня есть сильно оптимизированное приложение JavaScript, высокоинтегрированный графический редактор. Теперь я начал профилировать его (используя инструменты Chrome dev) с огромным количеством данных (тысячи фигур на графике), и я столкнулся с ранее необычным узким местом производительности 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            |

Это занимает 65% всего (!), Оставаясь узким местом монстра в моей кодовой базе. Я знаю, что это процесс отслеживания объекта под указателем, и у меня есть свои бесполезные идеи о том, как это можно оптимизировать (использовать меньшее количество элементов, использовать меньше событий мыши и т.д.).

Контекст: приведенный выше профиль производительности показывает функцию "панорамирования экрана" в моем приложении, где содержимое экрана можно перемещать, перетаскивая пустую область. Это приводит к множеству объектов, перемещаемых вокруг, оптимизированных путем перемещения их контейнера, а не каждого объекта отдельно. Я сделал демо.


Прежде чем перейти к этому, я хотел найти общие принципы оптимизации тестирования ударов (те хорошие статьи о блоге "Нет sh * t, Sherlock"), а также, если существуют какие-либо трюки для повышения производительности с этой целью (например, используя translate3d чтобы включить обработку GPU).

Я пробовал запросы, такие как js optimize hit test, но результаты полны статей графического программирования и примеров ручной реализации - это как если бы сообщество JS даже не слышало об этом раньше! Даже руководству хром devtools не хватает этой области.

Итак, вот я, с гордостью сделанный с моими исследованиями, спрашивая: как мне получить оптимизацию тестов на местное тестирование в JavaScript?


Я подготовил демоверсию, которая демонстрирует узкое место производительности, хотя это не совсем то же самое, что и мое фактическое приложение, и цифры, очевидно, будут меняться и на устройстве. Чтобы увидеть узкое место:

  1. Перейдите на вкладку "Временная шкала" в Chrome (или эквиваленте вашего браузера)
  2. Начните запись, затем развернитесь в демо, как сумасшедший
  3. Остановите запись и проверьте результаты

Резюме всех значительных оптимизаций, которые я уже сделал в этой области:

  • перемещение одного контейнера на экране вместо перемещения по тысячам элементов по отдельности
  • с помощью transform: translate3d для перемещения контейнера
  • v-синхронизация движения мыши с частотой обновления экрана
  • удаление всех возможных ненужных элементов "обертки" и "фиксатора"
  • использование pointer-events: none на фигурах - нет эффекта

Дополнительные замечания:

  • узкое место существует как с ускорением GPU, так и без него
  • тестирование было выполнено только в Chrome, последние
  • DOM создается с использованием ReactJS, но та же проблема наблюдается без него, как видно из связанной демонстрации

Ответ 1

Интересно, что pointer-events: none имеет никакого эффекта. Но если вы подумаете об этом, это имеет смысл, так как элементы с этим флагом все еще затушевывают события указателей других элементов, поэтому hittest должен иметь место в любом случае.

Что вы можете сделать, это наложить на критический контент и ответить на события мыши на этом оверлее, пусть ваш код решит, что с ним делать.

Это работает, потому что, как только алгоритм hittest нашел хит, и я предполагаю, что он делает это вниз, z-index, он останавливается.


С наложением

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

Ответ 2

Одна из проблем заключается в том, что вы перемещаете КАЖДЫЙ один элемент внутри вашего контейнера, неважно, есть ли у вас ускорение GPU или нет, шея бутылки пересчитывает новую позицию, то есть поле процессора.

Мое предложение состоит в том, чтобы сегментировать контейнеры, поэтому вы можете перемещать различные панели индивидуально, уменьшая нагрузку, это называется широкомасштабным вычислением, то есть только перемещать то, что нужно переместить. Если у вас что-то появилось на экране, зачем вам его перемещать?

Начните с создания вместо одного, 16 контейнеров, вам нужно будет сделать некоторую математику здесь, чтобы узнать, какая из этих панелей отображается. Затем, когда происходит событие мыши, переместите только те панели и оставьте те, которые не показаны, где они находятся. Это должно значительно сократить время, затрачиваемое на их перемещение.

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

В этом примере у нас есть 16 панелей, из которых 2 показаны (отмечены S для экрана). Когда пользователь поворачивается, проверьте ограничительную рамку "экрана", узнайте, какие панели относятся к "экрану", перемещайте только те панели. Это теоретически бесконечно масштабируемо.

К сожалению, мне не хватает времени, чтобы написать код, демонстрирующий эту мысль, но я надеюсь, что это поможет вам.

Ура!