改善大型桌面上的iScroll性能

时间:2017-04-19 19:40:58

标签: javascript jquery iscroll

我根据用户如何滚动以保持对齐方式,以编程方式更新表标题及其第一列位置。

我遇到的问题是,只要我的数据集足够大,滚动就会越来越不稳定/不太流畅。

相关代码位于小提琴的最底层:

iScroll.on('scroll', function(){
    var pos = $('#scroller').position();
    $('#pos').text('pos.left=' + pos.left + ' pos.top=' + pos.top);

    // code to hold first row and first column
    $('#scroller th:nth-child(1)').css({top: (-pos.top), left: (-pos.left), position:'relative'});
    $('#scroller th:nth-child(n+1)').css({top: (-pos.top), position:'relative'});

    // this seems to be the most expensive operation:
    $('#scroller td:nth-child(1)').css({left: (-pos.left), position:'relative'});
});

我知道通过缓存元素等可以更好地编写这些内容。例如,我已经尝试将元素保存到数组中并在更多" vanilla"中更新它们的位置。时尚:

headerElements[i].style.left = left + 'px'; // etc...

无论我多快进行回调,我对结果仍然不满意。你有什么建议吗?

https://jsfiddle.net/0qv1kjac/16/

8 个答案:

答案 0 :(得分:6)

只需使用ClusterizeJS!它可以处理数十万行,并为此目的构建完全

你问,它是如何运作的?

  

主要思想不是用所有使用的标签污染DOM。而不是 - 它将列表拆分为集群,然后显示当前滚动位置的元素,并在列表的顶部和底部添加额外的行以模拟表的完整高度,以便浏览器显示滚动条和完整列表

答案 1 :(得分:4)

为了能够处理大量数据,您需要数据虚拟化。但它有一些限制。

首先,您需要确定视口的大小。我们假设你要连续渲染10个项目,列中有20个项目。那将是10x20件。在你用id包装器调整它的div。

然后您需要知道您拥有的总数据量。从你的小提琴,它将是100x100项目。而且,您还需要知道项目(单元格)的高度和宽度。我们取40x120(以px为单位)。

所以 div#wrapper 是一个视口,它应该有固定大小的10x20项。然后,您需要为表格设置正确的宽度和高度。 的高度将等于列中的数据总量,包括逐项高度。 表格的宽度将是按行项目宽度排列的项目总数。

设置完毕后, div#wrapper 会收到水平和垂直滚动。现在你可以向左和向下滚动,但它只是空的空间。但是,这个空白区域可以容纳您拥有的确切数据量。

然后你需要将滚动数据保留为左上角(位置),以像素为单位并将其标准化为项目数量,这样你就可以知道你滚动了多少像素,但你知道多少项目&# 39;滚动(如果我们从上到下滚动,则为行)。

可以通过在项目高度上滚动的像素分割来完成。例如,您向左滚动了80px,即2个项目。这意味着这些项目应该是不可见的,因为您已经滚过它们。所以你知道你滚动了2个项目,你知道你应该连续看到10个项目。这意味着你获取包含100个项目的行数据的数据数组,并将其切片为:

var visibleItems = rowData.slice(itemsScrolled, itemsScrolled + 10);

它将为您提供在当前滚动位置的视口中应该可见的项目。一旦你有这些项目,你需要构建html并将其附加到表格。

同样在每个滚动事件中,您需要为 tbody和thead 设置顶部和左侧位置,以便它们随着滚动移动,否则您将获得数据,但它将在(0; 0) )在视口内。

无论如何,代码说了千言万语,所以这里是小提琴:https://jsfiddle.net/Ldfjrg81/9/

注意,这种方法要求高度和宽度要精确,否则会无法正常工作。此外,如果您有不同尺寸的物品,也应该考虑这一点,如果您有固定和相同尺寸的物品,那就更好了。在jsfiddle中,我注释掉了强制第一列保持原位的代码,但你可以单独渲染它。

根据评论中的建议,这是一个很好的解决方案,因为它会为你处理很多案例。

如果使用react.js或vue.js

,您可以更快地进行渲染

答案 2 :(得分:3)

这不是您正在寻找的答案,但无论如何这里是我的2美分。

Javascript动画(特别是考虑到DOM必须渲染的数量)永远不会像你想要的那样平滑。即使你可以在你的机器上顺利运行,也可能会在其他人(旧PC,浏览器等)上发生剧烈变化。

如果我自己要解决这个问题,我会看到两个选项。

  1. 去旧学校并添加水平和垂直滚动条。我知道这不是一个漂亮的解决方案,但它会运作良好。

  2. 仅渲染一定数量的行并将其丢弃在屏幕外。这可能有点复杂,但实质上你会渲染10行。一旦用户滚动到第11个应该在那里的点,渲染那个并删除第1个。你可以根据需要弹出它们。

  3. 就实际的JS(你提到将元素放入数组)而言,这没有帮助。实际的不稳定性是由于浏览器需要首先渲染那么多元素。

答案 3 :(得分:2)

由于scroll事件以非常高的速度触发,您会遇到不连贯/不平滑的滚动。

并且每次触发你都会调整多个元素的位置:这是昂贵的,而且直到浏览器完成重绘它之后才会显示出来。 s 无响应(这里不稳定的滚动)。

我看到两个选项:

第一个选项:只显示整个数据集的可见子集(这已在另一个答案中提出,所以我不会再进一步​​了)

选项二(更简单)

首先,让lefttop css更改的动画通过过渡发生。这样效率更高,非阻塞,并且经常让浏览器利用 gpu

然后,不要重复调整lefttop,而是暂时执行;例如0.5秒。这是通过ScrollWorker()函数setTimeout()完成的(见下面的代码)。

最后使用scroll事件调用的回调来更新#scroller位置(存储在变量中)。

// Position of the `#scroller` element
// (I used two globals that may pollute the global namespace
// that piece of code is just for explanation purpose)    

var oldPosition, 
    newPosition;


// Use transition to perform animations
// You may set this in the stylesheet

$('th').css( { 'transition': 'left 0.5s, top 0.5s' } );
$('td').css( { 'transition': 'left 0.5s, top 0.5s' } );


// Save the initial position

newPosition = $('#scroller').position();
oldPosition = $('#scroller').position();


// Upon scroll just set the position value

iScroll.on('scroll', function() {
    newPosition = $('#scroller').position();
} );


// Start the scroll worker

ScrollWorker();


function ScrollWorker() { 

    // Adjust the layout if position changed (your original code)

    if( newPosition.left != oldPosition.left || newPosition.top != oldPosition.top ) {
        $('#scroller th:nth-child(1)').css({top: (-newPosition.top), left: (-newPosition.left), position:'relative'});
        $('#scroller th:nth-child(n+1)').css({top: (-newPosition.top), position:'relative'});
        $('#scroller td:nth-child(1)').css({left: (-newPosition.left), position:'relative'});

        // Update the stored position

        oldPosition.left = newPosition.left;
        oldPosition.top  = newPosition.top;

        // Let animation complete then check again
        // You may adjust the timer value
        // The timer value must be higher or equal the transition time

        setTimeout( ScrollWorker, 500 );

    } else {

        // No changes
        // Check again after just 0.1secs

        setTimeout( ScrollWorker, 100 );
    }
}

以下是Fiddle

我将工作人员速度和过渡时间设置为 0.5秒。您可以使用更高或更低的时间调整值,最终以基于表中元素数量的动态方式调整。

答案 4 :(得分:1)

是的!以下是对JS Fiddle代码的一些改进。您可以在https://jsfiddle.net/briankueck/u63maywa/

查看我的修改

一些建议的改进是:

  1. 将JS图层中的position:relative值切换为CSS图层中的position:fixed
  2. 缩短jQuery DOM链,以便代码不会从根元素开始。每次$ lookup都会一直走过dom。卷轴现在是根元素。一切都使用.find()的元素,这会创建更短的树和& jQuery可以更快地遍历这些分支。
  3. 将日志记录代码移出DOM&进入console.log。我已经添加了一个调试开关来禁用它,因为您正在寻找桌面上最快的滚动。如果它运行得足够快,那么你总是可以重新启用它以在JSFiddle中看到它。如果你真的需要在iPhone上看到它,那么它可以添加到DOM中。虽然,可能没有必要看左边和左边的。 iPhone中的顶部位置值。
  4. 删除所有无法映射到jQuery对象的无关$值。像$ scroller这样的东西让$混淆了,因为后者是jQuery库,但前者不是。
  5. 使用let代替var切换到ES6语法将使您的代码看起来更现代。
  6. <th>标记中有一个新的左计算,您希望查看该计算。
  7. 已清除iScroll事件侦听器。对于position:fixed,热门<th>代码只需要应用top属性。左<td>标记只需要应用left属性。角落<th>需要同时拥有top&amp; left属性适用于它。
  8. 删除所有不必要的内容,例如用于记录目的的无关HTML标记。
  9. 如果您真的想要更多香草,请更改JS代码中实际.css().style.left= -pos.left + 'px';属性的.style.top= -pos.top + 'px';方法。
  10. 尝试使用WinMergeBeyond Compare之类的差异工具来比较您的版本中的代码与我编辑中的代码,以便您轻松查看差异。

    希望这会使滚动变得更加平滑,因为滚动事件不必处理它不需要做的任何事情......比如5个完整的DOM遍历查找,而不是3个短树搜索。

    享受! :)

    <强> HTML:

    <body>
    <div id="wrapper">
      <table id="scroller">
        <thead>
        </thead>
        <tbody>
        </tbody>
      </table>
     </div>
    </body>
    

    <强> CSS:

    /* ... only the relevant bits ... */
    
    thead th {
      background-color: #99a;
      min-width: 120px;
      height: 32px;
      border: 1px solid #222;
      position: fixed; /* New */
      z-index: 9;
    }
    
    thead th:nth-child(1) {/*first cell in the header*/
      border-left: 1px solid #222; /* New: Border fix */
      border-right: 2px solid #222; /* New: Border fix */
      position: fixed; /* New */
      display: block; /*seperates the first cell in the header from the header*/
      background-color: #88b;
      z-index: 10;
    }
    

    <强> JS:

    // main code
    let debug = false;
    
    $(function(){ 
      let scroller = $('#scroller');
      let top = $('<tr/>');
      for (var i = 0; i < 100; i++) {
        let left = (i === 0) ? 0 : 1;
        top.append('<th style="left:' + ((123*i)+left) + 'px;">'+ Math.random().toString(36).substring(7) +'</th>');
      }
      scroller.find('thead').append(top);
      for (let i = 0; i < 100; i++) {
        let row = $('<tr/>');
        for (let j = 0; j < 100; j++) {
          row.append('<td>'+ Math.random().toString(36).substring(7) +'</td>');
        }
        scroller.find('tbody').append(row);
      }
    
      if (debug) console.log('initialize iscroll');
      let iScroll = null;
      try {
        iScroll = new IScroll('#wrapper', { 
          interactiveScrollbars: true, 
          scrollbars: true, 
          scrollX: true, 
          probeType: 3, 
          useTransition:false, 
          bounce:false
        });
      } catch(e) {
        if (debug) console.error(e.name + ":" + e.message + "\n" + e.stack);
      }
    
      if (debug) console.log('initialized');
    
      iScroll.on('scroll', function(){
        let pos = scroller.position();
        if (debug) console.log('pos.left=' + pos.left + ' pos.top=' + pos.top);
    
        // code to hold first row and first column
        scroller.find('th').css({top:-pos.top}); // Top Row
        scroller.find('th:nth-child(1)').css({left:-pos.left}); // Corner
        scroller.find('td:nth-child(1)').css({left:-pos.left}); // 1st Left Column
      });
    });
    

答案 5 :(得分:0)

您是否有必要创建自己的卷轴?为什么不在HTML / CSS中设置数据样式,只使用overflow属性? JavaScript需要能够调整帧速率。我之前使用的是你的jFiddle,它与原生溢出处理程序一起工作得很好。

答案 6 :(得分:0)

在手册中找到了这个。可能不是你想听到的,但它是这样的:

IScroll是需要为每个滚动区域启动的类。如果不是设备CPU /内存强加的,那么每页中的iScroll数量没有限制。 尽量保持DOM尽可能简单。 iScroll使用硬件合成层,但硬件可以处理的元素有限制。

答案 7 :(得分:0)

性能下降的原因是您的滚动事件处理程序一次又一次地触发,而不是等待合理且不易察觉的间隔。

invocations of scroll event handler

屏幕截图显示了在跟踪事件处理程序触发的次数时发生的情况,同时滚动了几秒钟。计算量大的事件处理程序被解雇超过600次!这是每秒超过60次!!!

这可能看似违反直觉,但降低表格更新的频率将大大增加感知响应时间。如果您的用户滚动了几分之一秒,大约150毫秒,并且表格更新十次,在滚动期间冻结显示,最终结果远比表格仅更新三次并且流畅地移动而不是冻结。只是浪费了处理器刻录来更新浏览器可以处理的次数而不会冻结。

那么,你如何制作一个以最高频率发射的事件处理程序,例如每秒25次,即使它被更频繁地触发,比如每秒100次?

这种天真的方式是运行setInterval事件。这样更好,但效率也非常低。有一种更好的方法,通过设置延迟事件处理程序,并在再次设置之前将其清除,直到最小时间间隔已经过去。这样,它的运行频率不会超过最大所需频率。这是为什么发明``clearInterval''方法的一个主要案例。

这是实时工作代码:

https://jsfiddle.net/pgjvf7pb/7/

注意:像这样连续刷新时,标题列可能显示在位置之外。

我建议仅在滚动暂停约25ms左右时进行更新,而不是连续进行。这样,用户看起来标题列是动态计算的,也是固定到位的,因为它在滚动后立即显示,而不是似乎与数据一起滚动。

https://jsfiddle.net/5vcqv7nq/2/

逻辑是这样的:

事件处理程序之外的

变量

  // stores the scrolling operation for a tiny delay to prevent redundancy
  var fresh;

  // stores time of last scrolling refresh
  var lastfresh = new Date();
事件处理程序中的

操作

  // clears redundant scrolling operations before they are applied
  if (fresh) clearTimeout(fresh);

  var x = function() {
    // stores new time of scrolling refresh
    lastfresh = new Date();
    // perform scrolling update operations here...
  };

  // refresh instantly if it is more than 50ms out of date
  if (new Date() - lastfresh > 50) x();

  // otherwise, pause for half of that time to avoid wasted runs
  else fresh = setTimeout(x, 25);

演示:https://jsfiddle.net/pgjvf7pb/7/

我再次建议您删除立即刷新数据的代码行,然后删除其他条件,并简单地使用一行

 fresh = setTimeout(x, 25);

这将在任何滚动完成时立即计算标题列,并节省更多操作。我对JS Fiddle的第二个链接显示了这个样子,在这里:https://jsfiddle.net/5vcqv7nq/2/