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

时间:2016-03-21 10:54:31

标签: javascript jquery html css angularjs

我不久前为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操作。 为此,我决定将<script>标记添加到包含适用于.metrics > .layout-row的css的dom。

解决方案:

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操作,但渲染/绘制视图似乎是这里的瓶颈。

我试图创建某种虚拟滚动但是因为树结构对于不同的数据集是不同的并且可以具有“无限”数量的级别(每行可以包含新的ng-repeat中的子行)这是真的艰巨的任务。

我会很感激有关如何在不使用虚拟滚动条的情况下提高性能的任何想法。

修改

Chrome时间轴的屏幕截图显示,大部分时间的滚动都是通过渲染消耗的(我猜这是因为复杂的DOM结构)

enter image description here

编辑2:

我不会说我实现了绝对平滑的滚动,但我发现了一些显着提升性能的事情(其中一些并不明显,结果比我预期的那么小的变化后更好)。

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

3 个答案:

答案 0 :(得分:0)

我过去曾在非常大的桌子上遇到过类似的问题。因此,将 UX 方面的事情排除在等式之外 - 为了实现您的目标可能如下。与其保持第一张桌子固定并移动所有其他桌子,也许您可​​以尝试相反的方法。也就是,将表格保持在溢出的容器内,以允许水平滚动,并在垂直滚动时使用 translateY 第一列表格单元格移动。

按照我描述的方式,您可以检索第一个表格单元格的最大宽度(甚至更好,设置固定宽度),将每个单元格绝对定位在左侧 - 但要小心,相对容器应该在溢出表格,在每行的第二个表格单元格上设置一个 padding-left,等于第一个表格单元格,然后,当垂直滚动时,添加一个负 translateY 值。选择器应该很容易访问'.layout-row .table-cell:first-child'。

对于小幅优化,不要使用 .css(),而是尝试使用 .attr('style', value)。另外,我读到您有动态内容并且无法缓存它,但是,也许您应该考虑在每次操作 DOM 时缓存一次。缓存的选定元素仍然比在每个滚动/触摸移动事件中计算选择器要好。

最后但并非最不重要的一点是,您还可以添加某种去抖动技术,以便滚动不会在每个滚动帧上发生,但可能每 10 帧发生一次 - 它仍然是肉眼不可见的,或者在任何情况下都比所有这些重绘事件都会使浏览器陷入瓶颈。

答案 1 :(得分:0)

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

祝你好运

答案 2 :(得分:-2)

所以我自己也不是专家,但是我确实有一些想法值得一提。 如果我的理解正确,那么问题在于该项目需要越来越多的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)

在任何需要的地方。我不知道这是否有帮助,或者是否回答了您的问题,但至少有一些想法。祝你好运:)