优化 DOM 元素 (Chrome) 的本机命中测试
Optimizing native hit testing of DOM elements (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% (!),在我的代码库中仍然是一个巨大的瓶颈。我知道这是在指针 下跟踪 对象的过程,而且我对如何优化它有一些无用的想法(使用更少的元素,使用更少的鼠标事件等)。
Context: The above performance profile shows a "screen panning" feature in my app, where the contents of the screen can be moved around by dragging the empty area. This results in lots of objects being moved around, optimized by moving their container instead of each object individually. I made a demo.
在进入这个之前,我想搜索优化命中测试的一般原则(那些好的 ol'"No sh*t, Sherlock" 博客文章),以及是否存在任何提高性能的技巧(例如使用 translate3d
启用 GPU 处理)。
我试过像js optimize hit test, but the results are full of graphics programming articles and manual implementation examples -- it's as if the JS community hadn't even heard of this thing before! Even the chrome devtools guide这样的查询缺少这个区域。
- 编辑:有这个问题,但帮助不大:
所以我自豪地完成了我的研究,问:我如何在 JavaScript 中优化本机命中测试?
I prepared a demo 证明了性能瓶颈,尽管它与我的实际应用 不完全 相同,而且数字也明显因设备而异。查看瓶颈:
- 转到 Chrome(或浏览器的等效项)
上的“时间轴”选项卡
- 开始录制,然后像个疯子一样在演示中摇晃
- 停止记录并查看结果
回顾一下我在这方面已经完成的所有重要优化:
- 在屏幕上移动单个容器,而不是单独移动数千个元素
- 使用
transform: translate3d
移动容器
- 将鼠标移动同步到屏幕刷新率
- 删除所有可能不必要的 "wrapper" 和 "fixer" 元素
- 在形状上使用
pointer-events: none
-- 无效果
补充说明:
- 有和没有GPU加速
都存在瓶颈
- 测试仅在 Chrome 完成,最新
- DOM 是使用 ReactJS 呈现的,但没有它也可以观察到同样的问题,如链接演示中所示
其中一个问题是您要移动容器内的每个元素,是否有 GPU-acceleration 并不重要,瓶颈正在重新计算它们的新位置,即处理器场.
这里我的建议是把容器分段,这样就可以单独移动各个pane,减少负载,这叫做broad-phase计算,也就是只移动需要移动的东西。如果你从屏幕上得到了一些东西,你为什么要移动它?
首先制作 16 个容器而不是一个容器,您必须在这里做一些数学运算才能找出显示了哪些窗格。然后,当发生鼠标事件时,只移动那些窗格,将未显示的窗格留在原处。这应该会大大减少移动它们的时间。
+------+------+------+------+
| SS|SS | | |
| SS|SS | | |
+------+------+------+------+
| | | | |
| | | | |
+------+------+------+------+
| | | | |
| | | | |
+------+------+------+------+
| | | | |
| | | | |
+------+------+------+------+
在此示例中,我们有 16 个窗格,其中显示了 2 个(标记为 S 表示屏幕)。当用户平移时,检查 "screen" 的边界框,找出属于 "screen" 的窗格,仅移动那些窗格。这在理论上是无限可扩展的。
不幸的是我没有时间写代码来表达这个想法,但我希望这对你有所帮助。
干杯!
有趣的是,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>
无覆盖
// ================================================
// 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>
Chrome、content-visibility: auto
中现在有一个 CSS 属性,这有助于防止在 DOM 元素不可见时进行命中测试。参见 web.dev。
The content-visibility property accepts several values, but auto is the one that provides immediate performance improvements. An element that has content-visibility: auto gains layout, style and paint containment. If the element is off-screen (and not otherwise relevant to the user—relevant elements would be the ones that have focus or selection in their subtree), it also gains size containment (and it stops painting and hit-testing its contents).
我无法复制这个演示的问题,可能是因为 pointer-events: none
现在按预期工作,正如@rodrigo-cabral 提到的,但是我在使用 HTML5 拖动时遇到了重大问题拖放是因为有大量带有 dragOver
或 dragEnter
事件处理程序的元素,其中大部分都在屏幕外元素上(虚拟化这些元素有很大的缺点,所以我们还没有这样做还没有)。
向具有拖动事件处理程序的元素添加 content-visibility: auto
属性 显着缩短了命中测试时间(从 12 毫秒减少到 <2 毫秒)。
这确实有一些注意事项,例如使元素呈现为好像它们有 overflow: hidden
,或者要求在元素上设置 contain-intrinsic-size
以确保它们占用 [=34] =] 当它们在屏幕外时,但这是我发现的唯一 属性 有助于减少命中测试时间的方法。
注意:尝试单独使用 contain: layout style paint size
对减少命中测试时间没有任何影响。
我有一个高度优化的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% (!),在我的代码库中仍然是一个巨大的瓶颈。我知道这是在指针 下跟踪 对象的过程,而且我对如何优化它有一些无用的想法(使用更少的元素,使用更少的鼠标事件等)。
Context: The above performance profile shows a "screen panning" feature in my app, where the contents of the screen can be moved around by dragging the empty area. This results in lots of objects being moved around, optimized by moving their container instead of each object individually. I made a demo.
在进入这个之前,我想搜索优化命中测试的一般原则(那些好的 ol'"No sh*t, Sherlock" 博客文章),以及是否存在任何提高性能的技巧(例如使用 translate3d
启用 GPU 处理)。
我试过像js optimize hit test, but the results are full of graphics programming articles and manual implementation examples -- it's as if the JS community hadn't even heard of this thing before! Even the chrome devtools guide这样的查询缺少这个区域。
- 编辑:有这个问题,但帮助不大:
所以我自豪地完成了我的研究,问:我如何在 JavaScript 中优化本机命中测试?
I prepared a demo 证明了性能瓶颈,尽管它与我的实际应用 不完全 相同,而且数字也明显因设备而异。查看瓶颈:
- 转到 Chrome(或浏览器的等效项) 上的“时间轴”选项卡
- 开始录制,然后像个疯子一样在演示中摇晃
- 停止记录并查看结果
回顾一下我在这方面已经完成的所有重要优化:
- 在屏幕上移动单个容器,而不是单独移动数千个元素
- 使用
transform: translate3d
移动容器 - 将鼠标移动同步到屏幕刷新率
- 删除所有可能不必要的 "wrapper" 和 "fixer" 元素
- 在形状上使用
pointer-events: none
-- 无效果
补充说明:
- 有和没有GPU加速 都存在瓶颈
- 测试仅在 Chrome 完成,最新
- DOM 是使用 ReactJS 呈现的,但没有它也可以观察到同样的问题,如链接演示中所示
其中一个问题是您要移动容器内的每个元素,是否有 GPU-acceleration 并不重要,瓶颈正在重新计算它们的新位置,即处理器场.
这里我的建议是把容器分段,这样就可以单独移动各个pane,减少负载,这叫做broad-phase计算,也就是只移动需要移动的东西。如果你从屏幕上得到了一些东西,你为什么要移动它?
首先制作 16 个容器而不是一个容器,您必须在这里做一些数学运算才能找出显示了哪些窗格。然后,当发生鼠标事件时,只移动那些窗格,将未显示的窗格留在原处。这应该会大大减少移动它们的时间。
+------+------+------+------+
| SS|SS | | |
| SS|SS | | |
+------+------+------+------+
| | | | |
| | | | |
+------+------+------+------+
| | | | |
| | | | |
+------+------+------+------+
| | | | |
| | | | |
+------+------+------+------+
在此示例中,我们有 16 个窗格,其中显示了 2 个(标记为 S 表示屏幕)。当用户平移时,检查 "screen" 的边界框,找出属于 "screen" 的窗格,仅移动那些窗格。这在理论上是无限可扩展的。
不幸的是我没有时间写代码来表达这个想法,但我希望这对你有所帮助。
干杯!
有趣的是,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>
无覆盖
// ================================================
// 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>
Chrome、content-visibility: auto
中现在有一个 CSS 属性,这有助于防止在 DOM 元素不可见时进行命中测试。参见 web.dev。
The content-visibility property accepts several values, but auto is the one that provides immediate performance improvements. An element that has content-visibility: auto gains layout, style and paint containment. If the element is off-screen (and not otherwise relevant to the user—relevant elements would be the ones that have focus or selection in their subtree), it also gains size containment (and it stops painting and hit-testing its contents).
我无法复制这个演示的问题,可能是因为 pointer-events: none
现在按预期工作,正如@rodrigo-cabral 提到的,但是我在使用 HTML5 拖动时遇到了重大问题拖放是因为有大量带有 dragOver
或 dragEnter
事件处理程序的元素,其中大部分都在屏幕外元素上(虚拟化这些元素有很大的缺点,所以我们还没有这样做还没有)。
向具有拖动事件处理程序的元素添加 content-visibility: auto
属性 显着缩短了命中测试时间(从 12 毫秒减少到 <2 毫秒)。
这确实有一些注意事项,例如使元素呈现为好像它们有 overflow: hidden
,或者要求在元素上设置 contain-intrinsic-size
以确保它们占用 [=34] =] 当它们在屏幕外时,但这是我发现的唯一 属性 有助于减少命中测试时间的方法。
注意:尝试单独使用 contain: layout style paint size
对减少命中测试时间没有任何影响。