【问题标题】:Most performant way to transform/move large number of DOM elements转换/移动大量 DOM 元素的最高效方式
【发布时间】:2016-07-07 20:23:06
【问题描述】:

前段时间我为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 &gt; 上应用偏移量时,问题就开始了。

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

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

  /*...*/

}

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

在我的第二次尝试中,a 尝试尽可能多地避免 DOM 操作。 为此,我决定将&lt;script&gt; 标签添加到包含适用于.metrics &gt; .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结构)

编辑 2:

我不会说我实现了绝对平滑的滚动,但我发现了一些可以显着提高性能的地方(其中一些并不明显,经过这么小的改动,结果比我预期的要好)。

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

【问题讨论】:

  • 我建议查看虚拟重复 github.com/stackfull/angular-virtual-scroll
  • 你应该使用响应式的材料设计,并且会自动转换你的大表格
  • @koningdavid 我试图实现它,但问题是,我正在使用循环指令,所以每一行都可以有它自己的子行(和另一个 ng-repeat)所以它使它更多复杂的
  • 在移动设备上平滑滚动的关键部分之一是滚动整个文档,而不是使用 overflow: auto 滚动某个不同的容器。很难为不同的容器实现完全平滑的滚动。它适用于你的情况吗?是滚动整个文档还是一个不同的容器?
  • 如果您有大量数据,您可能希望显示大约 10 行,但它们纯粹是占位符...当用户滚动时...您确定他们理论上会拥有什么滚动到并放入该数据...到您的占位符中,仅将大量数据保留在内存中...并且仅在需要时呈现可见元素。

标签: javascript jquery html css angularjs


【解决方案1】:

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

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

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

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

【讨论】:

    【解决方案2】:

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

    祝你好运

    【讨论】:

      【解决方案3】:

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

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

      【讨论】:

      • 这很有趣
      猜你喜欢
      • 1970-01-01
      • 2019-11-27
      • 1970-01-01
      • 1970-01-01
      • 2022-08-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多