优化Chrome中DOM元素的本地命中测试

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

背景:上面的性能概要显示了我的应用程序中的“屏幕平移”功能,其中屏幕内容可以通过拖动空白区域来移动。这导致许多对象被移动,通过移动它们的容器而不是每个对象单独进行优化。 我制作了一个演示。


在开始之前,我想搜索有关优化命中测试的一般原则(那些老套的“废话”博客文章),以及是否存在任何技巧来提高此方面的性能(例如使用translate3d启用GPU处理)。我尝试了查询,如js optimize hit test,但结果充满了图形编程文章和手动实现示例 - 就好像JS社区甚至从未听说过这个东西!即使是Chrome devtools guide也缺乏这个领域。
编辑:有这个问题,但它没有多大帮助:What is the Chrome Dev Tools "Hit Test" timeline entry? 所以在完成我的研究后,我很自豪地问:如何优化JavaScript中的本机命中测试?

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

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

以下是相关优化的概述:
  • 将单个容器在屏幕上移动,而不是逐个移动数千个元素
  • 使用transform: translate3d移动容器
  • 将鼠标移动与屏幕刷新率同步
  • 删除所有可能不必要的"wrapper"和"fixer"元素
  • 在形状上使用pointer-events: none-- 没有效果
附加说明:
  • 瓶颈存在于有或无GPU加速的情况下
  • 测试仅在最新版Chrome中进行
  • DOM使用ReactJS渲染,但在链接的演示中也可以观察到相同的问题

有趣,这是 https://crbug.com/454909(“合成器不支持pointer-events:none”)还是https://bugs.chromium.org/p/chromium/issues/list?q=component:Blink%3EHitTesting下的其他问题? - Josh Lee
@JohnWeisz 你有没有考虑采用“懒加载”的方式来渲染你的节点,即“只渲染屏幕上可见的内容”?我认为这是处理大量节点时确保性能可靠的唯一方法。当然,这会迫使你编写更多的代码。 - Renan Le Caro
@RenanLeCaro 实际上是的,但不幸的是,DOM元素的重复添加和删除有更大的性能影响。 - John Weisz
这可能可以使用画布解决,但处理复杂的形状会很快变得棘手。 - Renan Le Caro
1
对于在2020年或之后阅读此内容的任何人,“pointer-events: none”现在可用。至少在Chrome 80上滚动已成为我的性能瓶颈。 - Rodrigo Cabral
3个回答

16

有趣的是,pointer-events: none没有效果。但如果你考虑一下,这很有道理,因为设置了该标志的元素仍会遮挡其他元素的指针事件,因此必须进行命中测试。

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

这有效的原因是一旦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>

没有覆盖层

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


1
太棒了 - 有40,000个元素,使用覆盖后,我的命中测试只需要50微秒,而不是25毫秒。享受你的奖励吧! - Ben Visness
是的,这是相当大的改进,很好地捕捉到了。 - John Weisz
是的,它将我的性能提升了300%。非常感谢您。 - Hiep Tran

6

问题在于您正在移动容器内的每个元素,无论您是否拥有GPU加速,瓶颈都是重新计算它们的新位置,这是处理器领域。

我的建议是对容器进行分段,因此您可以逐个移动各种窗格,减少负载,这称为广泛阶段计算,即只移动需要移动的内容。如果您的某些内容已经超出屏幕,那么为什么要将其移动呢?

首先,请创建16个容器而不是一个,需要做一些数学计算才能找到哪些窗格正在显示。然后,当发生鼠标事件时,仅移动那些窗格,将未显示的窗格保留在原位。这应该大大减少移动所需的时间。

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

在此示例中,我们有16个窗格,其中2个正在显示(由S表示)。当用户移动时,检查“屏幕”的边界框,找出哪些窗格属于“屏幕”,仅移动这些窗格。理论上可以无限扩展。
不幸的是我没有时间编写展示思路的代码,但我希望这能帮助您。
干杯!

你可能有一部分是正确的。由于容器内的div具有绝对定位,它们不需要重新计算其新位置。hitTest是在该容器上进行鼠标事件检测的。也许stopPropagation或类似的东西可以帮助解决这个问题。 - Kiran Shakya

4
现在在Chrome中有一个CSS属性content-visibility: auto,可以帮助防止当DOM元素不在视图中时的命中测试。有关详细信息,请参见web.dev

content-visibility属性接受多个值,但auto提供了即时的性能改进。设置content-visibility: auto的元素将获得布局、样式和绘制包含。如果该元素离开屏幕(且对用户没有其他相关性——相关元素将是其子树中具有聚焦或选择的元素),它还会获得大小包含(并停止绘制和命中测试其内容)。

我无法复制此演示的问题,可能是由于pointer-events: none如@rodrigo-cabral所提到的按预期工作,但是使用HTML5拖放时,由于存在大量具有dragOverdragEnter事件处理程序的元素,我遇到了显着的问题,其中大多数是在屏幕外的元素(虚拟这些元素会带来显着的缺点,因此我们尚未这样做)。
content-visibility: auto属性添加到具有拖动事件处理程序的元素中,可以显著提高命中测试时间(从12ms降至<2ms)。
这确实带有一些注意事项,例如会导致元素呈现为具有overflow: hidden的样式,或要求在元素上设置contain-intrinsic-size以确保它们在屏幕外时占用该空间,但这是我发现唯一有助于减少命中测试时间的属性。
注意:仅尝试使用contain: layout style paint size无法减少命中测试时间。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接