提高大型桌面上的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'}); }); 

我知道通过缓存元素等可以写得更有效。 例如,我尝试将元素保存到数组中,并以更“香草”的方式更新它们的位置:

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

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

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

只需使用ClusterizeJS ! 它可以处理数十万行,并且是为此目的而构建的。

你问,它是如何工作的?

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

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

首先,您需要确定视图端口的大小。 假设你要连续渲染10个项目,列中有20个项目。 那将是10×20项目。 在你用id包装器摆弄它的div。

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

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

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

然后你需要在左侧和顶部(位置)进行滚动数据,以像素为单位并将其标准化为项目数量,这样您就可以知道滚动的像素数量,以及滚动的项目数量(或行数)如果我们从上到下滚动)。

它可以通过在项目高度上滚动的像素分割来完成。 例如,您向左滚动了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美分。

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

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

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

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

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

您正在经历不连贯/不平滑的滚动,因为scroll事件以非常高的速度触发。

每次它激发你都在调整许多元素的位置:这是昂贵的,而且直到浏览器完成重绘它没有响应 (这里是不连贯的滚动)。

我看到两个选择:

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


选项二(更容易)

首先,让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 ); } } 

这是小提琴

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

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

一些建议的改进是:

  1. 切换position:relative JS层中的position:relativeposition:fixed在CSS层中position:fixed
  2. 缩短jQuery DOM链,以便代码不会从根元素开始并在每个$ lookup中一直遍历dom。 滚动条现在是根元素。 一切都使用.find()关闭该元素,这会创建更短的树,jQuery可以更快地遍历这些分支。
  3. 将日志记录代码移出DOM并进入console.log。 我已经添加了一个调试开关来禁用它,因为你正在寻找桌面上最快的滚动。 如果它运行得足够快,那么你总是可以重新启用它以在JSFiddle中看到它。 如果你真的需要在iPhone上看到它,那么它可以添加到DOM中。 虽然,可能没有必要在iPhone中看到左侧和顶部位置值。
  4. 删除所有无关的$值,这些值未映射到jQuery对象。 像$ scroller这样的东西会让$混淆,因为后者是jQuery库,但前者不是。
  5. 切换到ES6语法,使用let而不是var将使您的代码看起来更现代。
  6. 标签中有一个新的左计算,您需要查看该计算。

  7. 已清除iScroll事件侦听器。 在position:fixed ,top

    标签只需要将top属性应用于它们。 左

    标签只需要将left属性应用于它们。 角落

    需要同时应用topleft属性。

  8. 删除所有不必要的内容,例如用于记录目的的无关HTML标记。
  9. 如果你真的想要更多的香草,请更改实际的.style.left= -pos.left + 'px';.css()方法.style.left= -pos.left + 'px';.style.top= -pos.top + 'px'; JS代码中的属性。

尝试使用像WinMerge或Beyond Compare这样的差异工具来比较版本中的代码和编辑中的代码,以便您可以轻松地看到差异。

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

请享用! 🙂

HTML:

  

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 = $(''); for (var i = 0; i < 100; i++) { let left = (i === 0) ? 0 : 1; top.append(''+ Math.random().toString(36).substring(7) +''); } scroller.find('thead').append(top); for (let i = 0; i < 100; i++) { let row = $(''); for (let j = 0; j < 100; j++) { row.append(''+ Math.random().toString(36).substring(7) +''); } 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 }); }); 

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

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

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

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

调用scroll事件处理程序

屏幕截图显示了当我跟踪事件处理程序触发了多少次时,发生了什么,同时滚动了几秒钟。 计算量大的事件处理程序被解雇超过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/