优化DOM元素的原生命中测试(Chrome)



我有一个高度优化的JavaScript应用程序,一个高度交互式的图形编辑器。我现在开始用大量数据(图中有数千个形状)对它进行分析(使用Chrome开发工具),我遇到了以前不寻常的性能瓶颈,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%的所有内容(!),在我的代码库中仍然是一个巨大的瓶颈。我知道这是在指针下跟踪对象的过程,我对如何优化(使用更少的元素,使用更少的鼠标事件等)有一些无用的想法

上下文:以上性能配置文件显示了我的应用程序中的"屏幕平移"功能,可以通过拖动空白区域来移动屏幕内容。这导致许多对象被四处移动,通过移动它们的容器而不是单独移动每个对象来进行优化。我做了一个演示。


在跳到这之前,我想搜索优化命中测试的一般原则(那些好的"No sh*t,Sherlock"博客文章),以及是否存在任何提高性能的技巧(例如使用translate3d启用GPU处理)。

我尝试了像js optimize hit test这样的查询,但结果充满了图形编程文章和手动实现示例——就好像js社区以前甚至没有听说过这件事一样!甚至chrome devtools指南也缺少这方面的内容。

  • 编辑:有一个问题,但没有多大帮助:什么是Chrome开发工具"命中测试";时间线条目

因此,我自豪地完成了我的研究,并问道:如何优化JavaScript中的原生命中率测试


我准备了一个演示,演示了性能瓶颈,尽管它与我的实际应用程序并不完全相同,而且数字也会因设备而异。查看瓶颈:

  1. 转到Chrome(或等效浏览器)上的"时间轴"选项卡
  2. 开始录音,然后像疯子一样在演示中四处走动
  3. 停止录制并检查结果

我在这个领域已经做过的所有重要优化的回顾:

  • 在屏幕上移动单个容器,而不是单独移动数千个元素
  • 使用transform: translate3d移动容器
  • v同步鼠标移动到屏幕刷新率
  • 删除所有可能不必要的"wrapper"one_answers"fixer"元素
  • 在形状上使用pointer-events: none--没有效果

附加说明:

  • 瓶颈同时存在GPU加速
  • 测试只在Chrome中进行,最新
  • DOM是使用ReactJS呈现的,但如果没有它,也可以观察到同样的问题,如链接的演示中所示

有趣的是,pointer-events: none没有任何作用。但如果你仔细想想,这是有道理的,因为设置了该标志的元素仍然会模糊其他元素的指针事件,所以无论如何都必须进行击球。

你可以做的是在关键内容上覆盖一个覆盖层,并在该覆盖层上响应鼠标事件,让你的代码决定如何处理它

这之所以有效,是因为一旦hitest算法找到了一个命中,我假设它会沿着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>

其中一个问题是,你正在移动容器中的每一个元素,无论你是否有GPU加速,瓶颈都是重新计算它们的新位置,即处理器字段。

我在这里的建议是对容器进行分段,因此可以单独移动各个窗格,减少负载,这被称为宽阶段计算,也就是说,只移动需要移动的内容。如果你从屏幕上得到了什么,为什么要移动它?

从制作一个16个容器开始,你必须在这里做一些数学运算,找出显示的是这些窗格中的哪一个。然后,当鼠标事件发生时,只移动这些窗格,并将未显示的窗格留在它们所在的位置。这将大大减少移动它们所需的时间。

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

在这个例子中,我们有16个窗格,其中2个正在显示(用S表示屏幕)。当用户平移时,检查"屏幕"的边界框,找出哪些窗格属于"屏幕",只移动这些窗格。这在理论上是无限可扩展的。

不幸的是,我没有时间写代码来表达我的想法,但我希望这能帮助你。

干杯!

Chrome中现在有一个CSS属性content-visibility: auto,它有助于在DOM元素不在视图中时防止命中测试。参见网络版

content-visibility属性接受多个值,但auto可以立即提高性能。具有内容可见性的元素:自动获得布局、样式和绘画包含。如果元素在屏幕外(与用户无关——相关元素将是在子树中有焦点或选择的元素),它也会获得大小控制(并且停止绘制并点击测试其内容)。

正如@rodrigo-cabral所提到的,我无法复制这个演示的问题,可能是因为pointer-events: none现在按预期工作,但我在使用HTML5拖放进行拖动时遇到了重大问题,因为有大量元素带有dragOverdragEnter事件处理程序,其中大部分都是屏幕外的元素(虚拟化这些元素有很大的缺点,所以我们还没有这样做)。

content-visibility: auto属性添加到具有拖动事件处理程序的元素中,显著提高了命中测试时间(从12毫秒降低到<2毫秒)。

这确实有一些注意事项,例如使元素渲染为具有overflow: hidden,或者要求在元素上设置contain-intrinsic-size,以确保它们在屏幕外占据空间,但这是我发现的唯一有助于减少命中测试时间的属性。

注意:尝试单独使用contain: layout style paint size对减少命中测试时间没有任何影响。

最新更新