转换/移动大量DOM元素的最佳性能方式

25

我之前为AngularJS(带有Angular Material)构建了一个树形表格结构。

我的目标是仅在大屏幕上(1280及以上)使其起作用,但现在我想更新它并使其在更小的设备上(主要是平板电脑)工作,而不限制数据。 由于性能原因,我希望尽可能保持HTML的简单性(树状表格可以有1000多行,因此为单行创建更复杂的HTML将延长附加和呈现表格行所需的时间(行是动态的,因此不仅关乎初始呈现))。

我想到的一个主意是保留包含名称的第一个单元格的“固定”部分,并滚动包含所有指标的第二个部分,这些指标将被同步滚动。

当前的单行HTML:

<div class="tb-header layout-row">
  <div class="tb-title"><span>Name</span></div>
  <div class="tb-metrics">
     <div class="layout-row">
        <div class="tb-cell flex-10">812</div>
        <div class="tb-cell flex-7">621</div>
        <div class="tb-cell flex-4">76.5</div>
        <div class="tb-cell flex-7">289</div>
        <div class="tb-cell flex-4">46.5</div>
        <div class="tb-cell flex-7">308</div>
        <div class="tb-cell flex-4">49.6</div>
        <div class="tb-cell flex-7">390</div>
        <div class="tb-cell flex-4">48.0</div>
        <div class="tb-cell flex-7">190</div>
        <div class="tb-cell flex-4">23.4</div>
        <div class="tb-cell flex-7">0</div>
        <div class="tb-cell flex-4">0.0</div>
        <div class="tb-cell flex-8">6.4</div>
        <div class="tb-cell flex-8">0.0</div>
        <div class="tb-cell flex-8"></div>
     </div>
  </div>

我的想法是在父容器上使用touchmove事件(将整个树包装并绑定为指令),并检查当touchmove开始覆盖度量部分时,计算我应该移动度量的值。这部分工作得很好。

问题在于当我想将偏移应用于.tb-metrics >时。

我的第一次尝试是使用jQuery:

function moveMetrics( offset ) {
  var ofx = offset < 0 ? (offset < -getMetricsScrollWidth() ? -getMetricsScrollWidth() : offset) : 0;
  $('.tb-metrics').children().css('transform', 'translateX(' + ofx + 'px)');

  /*...*/

}

不幸的是,当表格包含更多行时,这个解决方案会变得非常缓慢(我无法缓存行因为它们是动态的)。

在我的第二次尝试中,我试图尽可能避免DOM操作。 为了实现这一点,我决定向dom添加<script>标签,其中包含应用于.metrics > .layout-row的css。

解决方案:

function moveMetrics( offset ) {
  var ofx = offset < 0 ? (offset < -getMetricsScrollWidth() ? -getMetricsScrollWidth() : offset) : 0
    , style = $( '#tbMetricsVirtualScroll' )
    ;

  if ( !style.length ) {
    body.append( '<style id="tbMetricsVirtualScroll">.tb-metrics > * {transform: translateX(' + ofx + 'px)}</style>' );
    style = $( '#tbMetricsVirtualScroll' );
  } else {
    style.text( '.tb-metrics > * {transform: translateX(' + ofx + 'px)}' );
  }
  /*...*/
}

然而,当表格包含大量行时,似乎并没有快多少。因此,瓶颈不是DOM操作,而是呈现/绘制视图。

我试图创建一种虚拟滚动的方式,但由于不同数据集的树结构不同,并且可以拥有“无限”级别(每行可以包含新的子行),这是一项非常困难的任务。

我会欣赏任何关于如何在不使用虚拟滚动的情况下改善性能的想法。

编辑:

Chrome时间线的截图显示,滚动的大部分时间都被渲染占用了(我猜这是因为复杂的DOM结构)。

enter image description here

第二次编辑:

我不会说我实现了完全流畅的滚动,但我找到了一些显着提高性能的方法(其中一些并不明显,还超出了我的期望)。

  • 简化类选择器: .tb-header > .tb-metrics > .tb-cell.tb-specific-cell 慢得多(似乎需要更长的时间来解析更复杂的选择器?)
  • 从变换元素中删除不透明度和框影
  • 尝试将变换元素分配到新图层中(使用CSS will-change 和/或 translateZ(0)


1
我建议看看"virtual repeat",它在这里:https://github.com/stackfull/angular-virtual-scroll - koningdavid
1
你应该使用材料设计,它是响应式的,并且会自动转换你的大表格。 - Amit Sirohiya
在移动设备上实现平滑滚动的关键之一是滚动整个文档,而不是某个带有overflow: auto属性的容器。对于一个独立的容器来说,要完全实现平滑滚动是很困难的。这是否适用于您的情况?您是滚动整个文档还是一个独立的容器? - Andrew Sklyarevsky
在那个项目中,我滚动整个页面,但最终我采用的解决方案是编写复杂的虚拟重复插件来处理树状结构。 - LJ Wadowski
1
如果你有大量的数据,你可能想要仅展示10行,但它们仅仅是占位符…当用户滚动时…你确定他们理论上将要滚动到哪里,并插入那些数据…到你的占位符中,只保留大量的数据在内存中…并仅在需要时渲染可见元素。 - scunliffe
显示剩余3条评论
3个回答

1
我过去遇到过类似的问题,主要是在处理大型表格方面。因此,不考虑用户体验的情况下,为了实现您的目标,可以尝试以下方法。与其保持第一个表格固定并移动其余所有表格,不如反过来。也就是说,将表格放在一个溢出容器中以允许水平滚动,并在垂直滚动时使用translateY移动第一列表格单元格。
按照我所描述的方式,您可以检索第一个表格单元格的最大宽度(或更好的方式是设置固定宽度),绝对定位每个左侧单元格 - 要小心,相对容器应该在溢出表格之外,设置每行第二个表格单元格的左内边距等于第一个表格单元格,然后在垂直滚动时添加负的translateY值。选择器应该很容易访问'.layout-row .table-cell:first-child'。
另外,为了进行小优化,可以尝试使用.attr('style', value)而不是.css()。此外,我看到您有动态内容且无法缓存它,但是,也许您应该考虑在每次操作DOM时缓存一次。缓存的选定元素仍然比在每个滚动/触摸移动事件中计算选择器要好。
最后,您还可以添加某种防抖技术,以便滚动不会在每个滚动帧上发生,而是可能每10帧发生一次 - 它仍然对眼睛不可见,或者在任何情况下都比使用所有这些重绘事件来瓶颈浏览器要好。

1
我建议您使用懒加载。 请注意,懒加载有4个选项: 实现懒加载设计模式的常见方法有四种:延迟初始化;虚拟代理;幽灵和值持有者。每种方法都有其优点和缺点(link)。
祝好运

-4

虽然我自己不是专家,但我有一些想法认为值得一提。如果我理解正确的话,问题在于这个项目需要越来越多的HTML元素,这会不断降低性能。

以下是我认为可以帮助你的3个JS概念:

*动态创建HTML元素:

let div = document.createElement('div');

动态创建您将用于元素的属性,并动态能够分配这些属性。
function attributeSetter(atr){
   div.setAttribute('class', atr)
}

这样,您可以在需要的任何地方调用函数,并动态地为其指定任何类名称。此外,您可以随意多次使用div元素。

最终概念。您还可以动态添加预定义的类,假设它们已经在css中预定义,像这样。

div.classList.add("my-class-name")

还有一个可能会派上用场的概念,那就是解构。

let myClasses = ['flex-10', 'flex-7', 'flex-4', 'flex-9'] //etc
let [flex10, flex7, flex4, flex9] = myClasses;

这将把所有那些字符串转换成变量,使你可以轻松地迭代它们并将它们动态添加到你的attributeSetter函数中,就像这样:

attributeSetter(flex10)

无论您在哪里需要它。我不知道这是否有帮助,或者是否回答了您的问题,但至少提供了一些想法。祝好运 :)

这很有趣。 - return_false

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