【问题标题】:Improving iScroll performance on a large table提高 iScroll 在大表上的性能
【发布时间】:2023-03-03 07:14:25
【问题描述】:

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

我遇到的问题是,一旦我的数据集变得足够大,滚动就会变得越来越不稳定/越来越不流畅。

相关代码在小提琴的最底部:

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/

【问题讨论】:

  • 您是否 100% 需要使用工具来创建滚动条?
  • 您可以创建一个数组,其中包含您的 html 块。根据当前滚动位置加载数组的每个索引,假设 pos 是前一个位置的正值。这应该做的是加载一大块数据(比如 100 行),并且在到达下一个“检查点”之前不会加载数据。 hth!

标签: javascript jquery iscroll


【解决方案1】:

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

你问它是如何工作的?

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

【讨论】:

    【解决方案2】:

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

    首先,您需要确定视口的大小。假设您要在一行中渲染 10 个项目,在列中渲染 20 个项目。那将是 10x20 个项目。在你摆弄它是带有 id 包装器的 div。

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

    所以 div#wrapper 是一个视口,它应该具有固定大小,如 10x20 项目。然后你需要为table设置正确的宽度和高度。 table 的高度将等于列中的数据总量,包括头部的项目高度。 table 的宽度将是按项目宽度计算的单行中的项目总数。

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

    然后您需要获取左侧和顶部(位置)的滚动数据,它以像素为单位并将其标准化为项目数量,因此您可以不知道您滚动了多少像素,而是知道您滚动了多少项目(如果我们从上到下滚动,则为行)。

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

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

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

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

    不管怎样,代码会说千言万语,所以这里是小提琴:https://jsfiddle.net/Ldfjrg81/9/

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

    按照 cmets 的建议,坚持使用某些库是一个很好的解决方案,因为它可以为您处理很多案例。

    如果使用 react.js 或 vue.js,您可以使渲染更快

    【讨论】:

      【解决方案3】:

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

      Javascript 动画(尤其是考虑到 DOM 必须渲染的数量)永远不会像您想要的那样流畅。即使您可以在您的机器上顺利完成它,它也有可能在其他人(旧电脑、浏览器等)上会有很大差异。

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

      1. 走老路,添加水平和垂直滚动条。我知道这不是一个很好的解决方案,但它会很好用。

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

      就实际的 JS(您提到将元素放入数组)而言,这无济于事。实际的不稳定是由于浏览器首先需要渲染那么多元素。

      【讨论】:

      • 获得一个实用的方法:关于提到的第二个选项,可能值得看看 Clusterize.js (clusterize.js.org),它只为您呈现列表的一部分。
      • “就实际的 JS(你提到将元素放入数组)而言,这没有帮助。”:这里的重点是 DOM API 比通过 jQuery 更快.我应该提到在最新的 Chrome 中加速这个对我来说已经足够了。即使我选择“1.”,我仍然需要像今天一样重新定位标题和第一列。 2 是一个可行的选择,但如果上面的代码可以以一种没有必要的方式进行优化,我仍然很感兴趣,因此是赏金 :)
      • 更改 JS 将(可能)影响很小,因为它似乎更像是一个图形渲染问题。我会看看提到的集群方法,这似乎是更合乎逻辑的方法
      • 如果您尝试过使用原生 JS 更新元素的位置,那么在这方面我看不到任何其他加快速度的方法。
      【解决方案4】:

      由于scroll 事件以非常高的速度触发,因此您的滚动不稳定/不流畅。

      并且每次触发您都在调整许多元素的位置:这很昂贵,而且在浏览器完成重绘之前它无响应(这里是断断续续的滚动)。

      我看到了两个选项:

      选项一:仅显示整个数据集的可见子集(这已在另一个答案中提出,因此我不再赘述)


      第二个选项(更简单)

      首先,让lefttop css 上的动画通过transitions 发生变化。这更有效,是非阻塞的,通常让浏览器利用 gpu

      然后不要重复调整lefttop,而是偶尔调整一次;例如 0.5 秒。这是由函数ScrollWorker()(参见下面的代码)完成的,该函数通过setTimeout() 调用自身。

      最后使用滚动事件调用的回调来保持#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

      我将 Worker 节奏和 transition 时间设置为 0.5 秒。您可以根据表中元素的数量以更高或更低的时间调整值,最终以动态方式调整。

      【讨论】:

        【解决方案5】:

        是的!以下是对 JS Fiddle 代码的一些改进。您可以通过以下方式查看我的编辑:https://jsfiddle.net/briankueck/u63maywa/

        一些建议的改进是:

        1. 将 JS 层中的 position:relative 值切换为 CSS 层中的 position:fixed
        2. 缩短 jQuery DOM 链,这样代码就不会从根元素开始,而是在每次 $ 查找时一直遍历 dom。滚动条现在是根元素。一切都使用该元素之外的.find(),这会创建更短的树,而 jQuery 可以更快地遍历这些分支。
        3. 将日志记录代码移出 DOM 并移入 console.log。我添加了一个调试开关来禁用它,因为您正在寻找桌面上最快的滚动。如果它对您来说运行得足够快,那么您可以随时重新启用它以在 JSFiddle 中看到它。如果你真的需要在 iPhone 上看到它,那么可以将它添加到 DOM 中。虽然,可能没有必要在 iPhone 中查看左侧和顶部位置值。
        4. 删除所有无关的 $ 值,它们没有映射到 jQuery 对象。 $scroller 之类的东西会与 $ 混淆,因为后者是 jQuery 库,但前者不是。
        5. 切换到 ES6 语法,使用 let 而不是 var 将使您的代码看起来更现代。
        6. <th> 标记中有一个新的左侧计算,您需要查看它。
        7. iScroll 事件监听器已被清理。使用position:fixed,顶部的<th> 标记只需要将top 属性应用于它们。左侧的<td> 标签只需要应用left 属性即可。角 <th> 需要同时应用 topleft 属性。
        8. 删除所有不必要的内容,例如用于记录目的的无关 HTML 标签。
        9. 如果您真的想要更普通,请在 JS 代码中将 .css() 方法更改为实际的 .style.left= -pos.left + 'px';.style.top= -pos.top + 'px'; 属性。

        尝试使用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
          });
        });
        

        【讨论】:

        • 感谢您的建议。一些很好的建议,虽然我并没有真正注意到任何性能改进。
        • 您也可以尝试摆脱 jQuery 的代码并尽可能使用原生 JS 调用。例如:$('#scroller'); 将变为 document.getElementById('scroller'); 为了提高滚动的平滑度,您需要将所有 scroller.find() 方法重写为原生 JS。您还可以在 JS 中使用 .style.left 和 .style.top 属性来避免使用 jQuery 的 .css() 方法。听起来好像只转换 iScroll.on('scroll', function(){ 事件监听器中的 5 条非调试行会给你带来最好的性能提升。
        • 是的,这就是我已经尝试过的。它确实有帮助 :) 我一直在寻找其他一些方法来改进它,但我想以更分页的方法为目标会更好......
        • 您似乎正在进入所谓的无限滚动。几年前(2006 年),我不得不构建一个可以处理 10,000 多行电子邮件的无限滚动条。要实现这一点(不使用库),请尝试在 DOM 的数据行数组上使用 .splice() 方法。这个想法是在 JS 层(模型)中维护第二个数组,然后只渲染到第一个数组,该数组适合视口内加上 & 减去行缓冲区。顶部和底部缓冲区充当触发器,允许拼接更多数据并通过 Ajax 加载缓冲数据。使用原生 JS 速度最快。
        【解决方案6】:

        您有必要创建自己的滚动条吗?为什么不直接在 HTML/CSS 中设置数据样式并使用 overflow 属性? JavaScript 需要改进其调整帧速率的能力。我之前使用的是你的 jFiddle,它与本机溢出处理程序配合得很好。

        【讨论】:

          【解决方案7】:

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

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

          【讨论】:

            【解决方案8】:

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

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

            这似乎违反直觉,但降低表更新频率将大大增加感知响应时间。如果您的用户滚动了不到一秒,大约 150 毫秒,并且表格更新了 10 次,在滚动期间冻结了显示,那么最终结果远比表格仅更新 3 次并且流畅地移动而不是冻结更糟糕.更新次数超过浏览器在不冻结的情况下可以处理的次数只是浪费处理器消耗。

            那么,您如何制作一个以最大频率触发的事件处理程序,例如每秒 25 次,即使触发频率更高,例如每秒 100 次?

            最简单的做法是运行 setInterval 事件。这更好,但也非常低效。有一种更好的方法,通过设置延迟事件处理程序,并在再次设置之前在后续调用中清除它,直到最小时间间隔过去。这样,它的运行频率不会超过所需的最大频率。这是为什么发明了“clearInterval”方法的一个主要案例。

            这是实时工作代码:

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

            注意:这样连续刷新时,标题栏可能会出现错位。

            我建议仅在滚动暂停大约 25 毫秒时才进行更新,而不是连续进行。这样,在用户看来,标题列是动态计算的并且是固定的,因为它在滚动后立即出现,而不是看起来与数据一起滚动。

            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/

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

             fresh = setTimeout(x, 25);
            

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

            【讨论】:

              猜你喜欢
              • 2014-04-10
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2012-04-19
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多