【问题标题】:Knockout.js incredibly slow under semi-large datasetsKnockout.js 在半大型数据集下非常慢
【发布时间】:2012-03-31 08:23:35
【问题描述】:

我刚刚开始使用 Knockout.js(一直想尝试一下,但现在我终于有了一个借口!) - 但是,在将表绑定到相对少量数据(大约 400 行左右)。

在我的模型中,我有以下代码:

this.projects = ko.observableArray( [] ); //Bind to empty array at startup

this.loadData = function (data) //Called when AJAX method returns
{
   for(var i = 0; i < data.length; i++)
   {
      this.projects.push(new ResultRow(data[i])); //<-- Bottleneck!
   }
};

问题是上面的for 循环大约需要 30 秒左右,大约有 400 行。但是,如果我将代码更改为:

this.loadData = function (data)
{
   var testArray = []; //<-- Plain ol' Javascript array
   for(var i = 0; i < data.length; i++)
   {
      testArray.push(new ResultRow(data[i]));
   }
};

然后for 循环在眨眼之间完成。换句话说,Knockout 的observableArray 对象的push 方法非常慢。

这是我的模板:

<tbody data-bind="foreach: projects">
    <tr>
       <td data-bind="text: code"></td>
       <td><a data-bind="projlink: key, text: projname"></td>
       <td data-bind="text: request"></td>
       <td data-bind="text: stage"></td>
       <td data-bind="text: type"></td>
       <td data-bind="text: launch"></td>
       <td><a data-bind="mailto: ownerEmail, text: owner"></a></td>
    </tr>
</tbody>

我的问题:

  1. 这是将我的数据(来自 AJAX 方法)绑定到可观察集合的正确方法吗?
  2. 我希望push 每次调用它时都会进行一些繁重的重新计算,例如可能重建绑定的 DOM 对象。有没有办法延迟重新计算,或者一次推送我的所有项目?

如果需要,我可以添加更多代码,但我很确定这是相关的。在大多数情况下,我只是关注网站上的 Knockout 教程。

更新:

根据以下建议,我更新了我的代码:

this.loadData = function (data)
{
   var mappedData = $.map(data, function (item) { return new ResultRow(item) });
   this.projects(mappedData);
};

但是,this.projects() 对于 400 行仍然需要大约 10 秒。我承认我不确定 没有 Knockout(只是通过 DOM 添加行)会有多快,但我感觉它会比 10 秒快得多。

更新 2:

根据下面的其他建议,我尝试了 jQuery.tmpl(KnockOut 原生支持),这个模板引擎将在 3 秒多的时间内绘制大约 400 行。这似乎是最好的方法,缺少一种在您滚动时动态加载更多数据的解决方案。

【问题讨论】:

  • 您是使用淘汰赛foreach 绑定还是使用foreach 的模板绑定。我只是想知道使用模板并包含 jquery tmpl 而不是本机模板引擎是否会有所作为。
  • @MikeChristensen - Knockout 拥有自己的与 (foreach, with) 绑定相关的本机模板引擎。它还支持其他模板引擎,即 jquery.tmpl。阅读here 了解更多详情。我没有对不同的引擎进行任何基准测试,所以不知道它是否会有所帮助。阅读您之前的评论,在 IE7 中您可能很难获得您所追求的性能。
  • 考虑到我们几个月前刚刚获得 IE7,我认为 IE9 将在 2019 年夏天左右推出。哦,我们也都在使用 WinXP.. Blech。
  • p.s.,看起来很慢的原因是您要向该可观察数组单独添加 400 个项目。对于可观察对象的每次更改,都必须为依赖于该数组的任何内容重新渲染视图。对于复杂的模板和许多要添加的项目,当您可以通过将数组设置为不同的实例来一次更新数组时,这会产生很多开销。至少在那时,重新渲染将完成一次。
  • 我找到了一种更快、更整洁的方法(没有开箱即用)。使用valueHasMutated 就可以了。如果你有时间,请检查答案。

标签: javascript performance knockout.js


【解决方案1】:

请看:Knockout.js Performance Gotcha #2 - Manipulating observableArrays

更好的模式是获取对我们底层数组的引用,推送到它,然后调用 .valueHasMutated()。现在,我们的订阅者只会收到一个通知,表明数组已更改。

【讨论】:

    【解决方案2】:

    按照 cmets 的建议。

    Knockout 有它自己的与 (foreach, with) 绑定相关的本机模板引擎。它还支持其他模板引擎,即 jquery.tmpl。阅读here 了解更多详情。我没有对不同的引擎进行任何基准测试,所以不知道它是否会有所帮助。阅读您之前的评论,在 IE7 中您可能很难获得您所追求的性能。

    顺便说一句,如果有人为它编写了适配器,KO 支持任何 js 模板引擎。您可能想尝试其他人,因为 jquery tmpl 将被 JsRender 取代。

    【讨论】:

    • jquery.tmpl 的性能越来越好,所以我会使用它。如果我有一些额外的时间,我可能会研究其他引擎以及编写自己的引擎。谢谢!
    • @MikeChristensen - 你还在 jQuery 模板中使用 data-bind 语句,还是使用 ${ code } 语法?
    • @ericb - 使用新代码,我使用${code} 语法,而且速度更快。我也一直在尝试让 Underscore.js 工作,但还没有运气(&lt;% .. %&gt; 语法会干扰 ASP.NET),而且似乎还没有 JsRender 支持。
    • @MikeChristensen - 好的,那么这是有道理的。 KO 的原生模板引擎不一定那么低效。当您使用 ${ code } 语法时,您不会对这些元素进行任何数据绑定(这会提高性能)。因此,如果您更改 ResultRow 的属性,它不会更新 UI(您必须更新 projects observableArray,这将强制重新呈现您的表)。如果您的数据几乎是只读的,那么 ${ } 绝对是有利的
    • 死灵术! jquery.tmpl 不再处于开发阶段
    【解决方案3】:

    在 KO 中使用分页 除了使用 $.map。

    在我使用带有剔除的分页之前,我在处理包含 1400 条记录的大型数据集时遇到了同样的问题。使用$.map 加载记录确实有很大的不同,但 DOM 渲染时间仍然很糟糕。然后我尝试使用分页,这使我的数据集快速点亮并且更加用户友好。 50 的页面大小使数据集的压倒性大大降低,并显着减少了 DOM 元素的数量。

    用 KO 很容易做到:

    http://jsfiddle.net/rniemeyer/5Xr2X/

    【讨论】:

      【解决方案4】:

      KnockoutJS 有一些很棒的教程,尤其是 the one about loading and saving data

      在他们的案例中,他们使用getJSON() 提取数据,速度非常快。从他们的例子:

      function TaskListViewModel() {
          // ... leave the existing code unchanged ...
      
          // Load initial state from server, convert it to Task instances, then populate self.tasks
          $.getJSON("/tasks", function(allData) {
              var mappedTasks = $.map(allData, function(item) { return new Task(item) });
              self.tasks(mappedTasks);
          });    
      }
      

      【讨论】:

      • 绝对是一个很大的改进,但是self.tasks(mappedTasks) 需要大约 10 秒才能运行(400 行)。我觉得这仍然不能接受。
      • 我同意 10 秒是不可接受的。使用 knockoutjs,我不确定什么比地图更好,所以我会收藏这个问题并期待更好的答案。
      • 好的。答案绝对值得+1 来简化我的代码并显着提高速度。也许有人对瓶颈是什么有更详细的解释。
      【解决方案5】:

      KoGrid看看。它可以智能地管理您的行渲染,从而提高性能。

      如果您尝试使用 foreach 绑定将 400 行绑定到一个表,那么您将很难通过 KO 将这么多行推入 DOM。

      KO 使用 foreach 绑定做了一些非常有趣的事情,其中​​大部分都是非常好的操作,但随着数组大小的增长,它们确实开始在性能上崩溃。

      我一直在尝试将大型数据集绑定到表/网格的漫长黑暗道路上,而您最终需要在本地拆分/分页数据。

      KoGrid 做到了这一切。它被构建为仅呈现查看者可以在页面上看到的行,然后虚拟化其他行直到需要它们。我想你会发现它在 400 件物品上的表现比你所体验的要好得多。

      【讨论】:

      • 这似乎在 IE7 上完全被破坏了(所有示例都不起作用),否则这会很棒!
      • 很高兴看到它 - KoGrid 仍在积极开发中。但是,这是否至少回答了您关于性能的问题?
      • 是的!它证实了我最初的怀疑,即默认的 KO 模板引擎非常慢。如果您需要任何人为您试验 KoGrid,我很乐意。听起来正是我们需要的!
      • 该死。这看起来真的很好!不幸的是,超过 50% 的应用程序用户使用 IE7!
      • 有意思,现在我们只好勉强支持IE11。在过去的 7 年里,情况有所改善。
      【解决方案6】:

      在呈现非常大的数组时避免锁定浏览器的一种解决方案是“限制”数组,以便一次只添加几个元素,并在两者之间进行休眠。这是一个可以做到这一点的函数:

      function throttledArray(getData) {
          var showingDataO = ko.observableArray(),
              showingData = [],
              sourceData = [];
          ko.computed(function () {
              var data = getData();
              if ( Math.abs(sourceData.length - data.length) / sourceData.length > 0.5 ) {
                  showingData = [];
                  sourceData = data;
                  (function load() {
                      if ( data == sourceData && showingData.length != data.length ) {
                          showingData = showingData.concat( data.slice(showingData.length, showingData.length + 20) );
                          showingDataO(showingData);
                          setTimeout(load, 500);
                      }
                  })();
              } else {
                  showingDataO(showingData = sourceData = data);
              }
          });
          return showingDataO;
      }
      

      根据您的用例,这可能会带来巨大的用户体验改进,因为用户可能只能在滚动之前看到第一批行。

      【讨论】:

      • 我喜欢这个解决方案,但不是每次迭代都 setTimeout,我建议只每 20 次或更多次迭代运行一次 setTimout,因为每次加载也需要很长时间。我看到您使用 +20 这样做,但乍一看对我来说并不明显。
      【解决方案7】:

      在我的情况下,利用 push() 接受可变参数可以提供最佳性能。 1300 行加载了 5973 毫秒(约 6 秒)。通过此优化,加载时间降至 914 毫秒( 那是 84.7 % 的改进!

      更多信息Pushing items to an observableArray

      this.projects = ko.observableArray( [] ); //Bind to empty array at startup
      
      this.loadData = function (data) //Called when AJAX method returns
      {
         var arrMappedData = ko.utils.arrayMap(data, function (item) {
             return new ResultRow(item);
         });
         //take advantage of push accepting variable arguments
         this.projects.push.apply(this.projects, arrMappedData);
      };
      

      【讨论】:

        【解决方案8】:

        我一直在处理大量的数据,valueHasMutated 工作就像一个魅力。

        查看模型:

        this.projects([]); //make observableArray empty --(1)
        
        var mutatedArray = this.projects(); -- (2)
        
        this.loadData = function (data) //Called when AJAX method returns
        {
        ko.utils.arrayForEach(data,function(item){
            mutatedArray.push(new ResultRow(item)); -- (3) // push to the array(normal array)  
        });  
        };
         this.projects.valueHasMutated(); -- (4) 
        

        调用(4)后,数组数据会自动加载到需要的observableArray中,即this.projects

        如果你有时间看看这个,以防万一有任何麻烦告诉我

        技巧:通过这样做,如果在推送级别可以避免任何依赖项(计算、订阅等),我们可以在调用(4) 后让它们一次性执行.

        【讨论】:

        • 问题不是对push 的调用太多,问题是即使是单个调用push 也会导致渲染时间过长。如果一个数组有 1000 个项目绑定到 foreach,则推送单个项目会重新渲染整个 foreach,并且需要付出大量的渲染时间成本。
        【解决方案9】:

        结合使用 jQuery.tmpl 的一种可能的解决方法是使用 setTimeout 以异步方式将项目一次推送到可观察数组;

        var self = this,
            remaining = data.length;
        
        add(); // Start adding items
        
        function add() {
          self.projects.push(data[data.length - remaining]);
        
          remaining -= 1;
        
          if (remaining > 0) {
            setTimeout(add, 10); // Schedule adding any remaining items
          }
        }
        

        这样,当你一次只添加一个项目时,浏览器/knockout.js 可以花时间相应地操作 DOM,而不会让浏览器完全阻塞几秒钟,这样用户就可以滚动同时列出。

        【讨论】:

        • 这将强制进行 N 次 DOM 更新,这将导致总渲染时间比一次执行所有操作要长得多。
        • 这当然是正确的。然而,关键是 N 是一个很大的数字并且将一个项目推入项目数组中触发大量其他 DOM 更新或计算的组合可能会导致浏览器冻结并让您关闭选项卡。通过设置每个项目或每 10、100 或其他数量的项目的超时,浏览器仍将响应。
        • 我会说这是错误的方法,在一般情况下,总更新不会冻结浏览器,但它是在所有其他失败时使用的东西。对我来说,这听起来像是一个写得很糟糕的应用程序,应该解决性能问题而不是让它不冻结。
        • 当然在一般情况下这是错误的方法,没有人会不同意你的看法。如果您需要执行大量 DOM 操作,这是一个 hack 和概念验证,用于防止浏览器冻结。几年前,我在列出几个大型 HTML 表格时需要它,每个单元格有多个绑定,导致评估数千个绑定,每个绑定都会影响 DOM 的状态。暂时需要该功能,以验证将基于 Excel 的桌面应用程序重新实现为 Web 应用程序的正确性。然后这个解决方案完美地解决了。
        • 该评论主要是供其他人阅读,而不是认为这是首选方式。我假设你知道自己在做什么。
        【解决方案10】:

        我一直在试验性能,我希望有两个贡献可能有用。

        我的实验集中在 DOM 操作时间上。所以在开始之前,绝对值得遵循上面关于在创建可观察数组之前推入 JS 数组等的要点。

        但如果 DOM 操作时间仍然妨碍您,那么这可能会有所帮助:


        1:一种将加载微调器包裹在慢速渲染周围的模式,然后使用 afterRender 隐藏它

        http://jsfiddle.net/HBYyL/1/

        这并不能真正解决性能问题,但表明如果您循环数以千计的项目,延迟可能是不可避免的,并且它使用的模式可以确保在长时间 KO 操作之前出现加载微调器,然后隐藏它。所以它至少改善了用户体验。

        确保您可以加载微调器:

        // Show the spinner immediately...
        $("#spinner").show();
        
        // ... by using a timeout around the operation that causes the slow render.
        window.setTimeout(function() {
            ko.applyBindings(vm)  
        }, 1)
        

        隐藏微调器:

        <div data-bind="template: {afterRender: hide}">
        

        触发:

        hide = function() {
            $("#spinner").hide()
        }
        

        2:使用 html 绑定作为 hack

        我记得当我在使用 Opera 开发机顶盒时使用 DOM 操作构建 UI 时的一项旧技术。它的速度非常慢,因此解决方案是将大块 HTML 存储为字符串,并通过设置 innerHTML 属性来加载字符串。

        类似的事情可以通过使用 html 绑定和一个计算来实现,该计算将表格的 HTML 派生为一大块文本,然后一次性应用它。这确实解决了性能问题,但最大的缺点是它严重限制了您可以在每个表行内进行绑定的操作。

        这里有一个展示这种方法的小提琴,以及一个可以从表行内部调用的函数,以一种类似于 KO 的方式删除一个项目。显然这不如正确的 KO 好,但如果您真的需要出色的性能,这是一种可能的解决方法。

        http://jsfiddle.net/9ZF3g/5/

        【讨论】:

          【解决方案11】:

          如果使用 IE,请尝试关闭开发工具。

          在 IE 中打开开发人员工具会显着减慢此操作的速度。我正在向数组中添加约 1000 个元素。打开开发工具时,这大约需要 10 秒,并且 IE 会在它发生时冻结。当我关闭开发工具时,操作是即时的,而且我在 IE 中没有看到减速。

          【讨论】:

            【解决方案12】:

            我还注意到 Knockout js 模板引擎在 IE 中运行速度较慢,我将其替换为 underscore.js,运行速度更快。

            【讨论】:

            • 请问你是怎么做到的?
            • @StuHarper 我导入了下划线库,然后在 main.js 中我按照knockoutjs.com/documentation/template-binding.html 的下划线集成部分所述的步骤进行操作
            • 哪个版本的 IE 出现了这种改进?
            • @bkwdesign 我使用的是 IE 10、11。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2013-11-21
            • 2017-02-07
            • 2019-03-07
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多