Оптимизация тестирования удаленных элементов 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?
Я подготовил демоверсию, которая демонстрирует узкое место производительности, хотя это не совсем то же самое, что и мое фактическое приложение, и цифры, очевидно, будут меняться и на устройстве. Чтобы увидеть узкое место:
- Перейдите на вкладку "Временная шкала" в Chrome (или эквиваленте вашего браузера)
- Начните запись, затем развернитесь в демо, как сумасшедший
- Остановите запись и проверьте результаты
Резюме всех значительных оптимизаций, которые я уже сделал в этой области:
- перемещение одного контейнера на экране вместо перемещения по тысячам элементов по отдельности
- с помощью
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 для экрана). Когда пользователь поворачивается, проверьте ограничительную рамку "экрана", узнайте, какие панели относятся к "экрану", перемещайте только те панели. Это теоретически бесконечно масштабируемо.
К сожалению, мне не хватает времени, чтобы написать код, демонстрирующий эту мысль, но я надеюсь, что это поможет вам.
Ура!