在大数据集下,Knockout.js的运行速度极其慢


86

我刚刚开始使用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眨眼间就完成了循环。换句话说,push敲除observableArray对象的方法非常慢。

这是我的模板:

<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行。这似乎是最好的方法,缺少一种解决方案,该解决方案可以在滚动时动态加载更多数据。


1
您是在使用敲除foreach绑定还是将模板与foreach绑定。我只是想知道是否使用模板并包括jquery tmpl而不是本机模板引擎会有所作为。
madcapnmckay 2012年

1
@MikeChristensen-淘汰赛具有与(foreach,with)绑定关联的自己的本机模板引擎。它还支持其他模板引擎,即jquery.tmpl。在此处阅读更多信息。我没有使用不同的引擎进行任何基准测试,所以不知道是否有帮助。阅读您以前的评论,在IE7中,您可能难以获得想要的性能。
madcapnmckay 2012年

2
考虑到几个月前我们刚刚获得IE7,我认为IE9将于2019年夏季左右推出。哦,我们也都在WinXP上。.Blech。
Mike Christensen 2012年

1
PS,它看起来很慢的原因是,您要添加400个项目可观察的阵列分别。对于可观察对象的每次更改,都必须为依赖于该数组的任何内容重新渲染视图。对于复杂的模板和要添加的许多项目,当您可以通过将数组设置为另一个实例来一次更新所有数组时,这会产生大量开销。至少到那时,重新渲染将执行一次。
Jeff Mercado 2012年

1
我发现了一种更快,更整洁的方法(什么都没有)。使用valueHasMutated做它。如果有时间,请检查答案。
非常酷

Answers:


16

如评论中所建议。

淘汰赛具有与(foreach,with)绑定相关联的自己的本机模板引擎。它还支持其他模板引擎,即jquery.tmpl。在此处阅读更多信息。我没有使用不同的引擎进行任何基准测试,所以不知道是否有帮助。阅读您以前的评论,在IE7中,您可能难以获得想要的性能。

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


我的性能越来越好,jquery.tmpl所以我会用它。如果有更多时间,我可能会研究其他引擎以及编写自己的引擎。谢谢!
Mike Christensen 2012年

1
@MikeChristensen-您仍在使用data-bindjQuery模板中的语句,还是使用$ {code}语法?
ericb'3

@ericb-在新代码中,我正在使用${code}语法,而且速度要快得多。我也一直在尝试使Underscore.js工作,但是还没有运气(<% .. %>语法干扰ASP.NET),并且似乎还没有JsRender支持。
Mike Christensen

1
@MikeChristensen-好的,这很有道理。KO的本机模板引擎不一定效率低下。使用$ {code}语法时,这些元素上没有任何数据绑定(这会提高性能)。因此,如果更改a的属性ResultRow,则不会更新UI(必须更新projectsobservableArray,这将强制重新呈现表)。如果您的数据几乎是只读的,$ {}绝对是有利的
ericb 2012年

4
死灵法师!jquery.tmpl不再开发中
Alex Larzelere13 2013年


13

除了使用$ .map之外,还可以对KO使用分页

在使用具有剔除功能的分页之前,我对1400条记录的大型数据集存在相同的问题。使用$.map加载记录确实产生了巨大的变化,但是DOM渲染时间仍然令人毛骨悚然。然后,我尝试使用分页,这使我的数据集的光照速度变得更快,而且更加用户友好。页面大小为50,使数据集不那么泛滥,并且大大减少了DOM元素的数量。

使用KO非常容易:

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


11

KnockoutJS有一些很棒的教程,尤其是关于加载和保存数据的教程。

在他们的情况下,他们使用数据的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);
    });    
}

1
绝对是一个很大的改进,但是self.tasks(mappedTasks)运行大约需要10秒钟(400行)。我觉得这还是不能接受的。
Mike Christensen 2012年

我同意10秒是不可接受的。我不确定使用淘汰表,哪个比地图更好,所以我会偏爱这个问题并寻找更好的答案。
deltree 2012年

1
好。+1既可以简化我的代码,又可以大大提高速度,答案肯定是正确的。也许有人对瓶颈有更详细的解释。
Mike Christensen 2012年

9

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

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

KO使用foreach绑定做了一些非常有趣的事情,其中大多数都是非常好的操作,但是随着数组大小的增长,它们的确开始降低性能。

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

KoGrid完成了所有这一切。它被构建为仅呈现查看者可以在页面上看到的行,然后虚拟化其他行,直到需要它们为止。我认为您会发现它在400种物品上的性能比您所体验的要好得多。


1
在IE7上,这似乎已被完全打破(没有示例工作),否则就太好了!
Mike Christensen 2012年

很高兴研究它-KoGrid仍在积极开发中。但是,这至少可以回答您有关性能的问题吗?
ericb 2012年

1
对!它证实了我最初的怀疑,即默认的KO模板引擎非常慢。如果您需要任何人为您做豚鼠KoGrid,我将很高兴。听起来正是我们所需要的!
Mike Christensen 2012年

真是 这看起来真的很好!不幸的是,我应用程序的用户中有50%以上使用IE7!
Jim G.

有趣的是,如今我们不得不勉强地支持IE11。在过去的7年中,情况有所改善。
MrBoJangles

5

避免在渲染非常大的数组时锁定浏览器的解决方案是“限制”数组,以便一次仅添加几个元素,并且在两者之间进行睡眠。这是一个可以做到的功能:

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;
}

根据您的用例,这可能会导致UX的大幅改进,因为用户可能只需要在滚动之前看到第一批行。


我喜欢这种解决方案,但建议不要每隔20次或更多次迭代运行setTimout,因为每次加载时间都太长,因此建议不要运行setTimout。我看到您正在使用+20进行此操作,但是乍一看对我来说并不明显。
charlierlee19年

5

在我的案例中,利用push()接受变量参数的性能最佳。1300行加载了5973毫秒(〜6秒)。通过这种优化,加载时间减少到了914ms(<1秒)。
改进了84.7%!

有关将项目推送到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);
};

4

我一直在处理海量的数据 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)


1
问题不是对的调用太多push,问题在于,即使是单个push调用也会导致较长的渲染时间。如果数组具有绑定到的1000个项目foreach,则推送单个项目会重新渲染整个foreach,您将付出大量渲染时间。
2016年

1

结合使用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,而不会完全阻塞浏览器几秒钟,以便用户可以同时滚动列表。


2
这将强制进行N次DOM更新,这将导致总渲染时间比一次完成所有操作都要长得多。
Fredrik C

当然是正确的。但是,要点是,N很大并且将项目推入项目数组的组合会触发大量其他DOM更新或计算,这可能导致浏览器冻结并为您提供杀死选项卡的机会。通过使每个项目或每10、100或其他一些项目有一个超时,浏览器仍将响应。
gna 2015年

2
我会说这是错误的方法,在这种情况下,总更新不会冻结浏览器,但是在所有其他更新失败时可以使用。对我来说,这听起来像是一个写得不好的应用程序,在该应用程序中,应该解决性能问题,而不仅仅是使其不冻结。
Fredrik C

1
当然,在一般情况下这是错误的方法,没有人会对此表示反对。如果需要执行DOM操作,这是一种防止浏览器冻结的技巧和概念证明。几年前,我列出了几个具有每个单元格多个绑定的大型HTML表时,需要使用它,导致评估了成千上万个绑定,每个绑定都影响DOM的状态。暂时需要该功能,以验证将基于Excel的桌面应用程序重新实现为Web应用程序的正确性。然后,此解决方案完美地解决了。
gna 2015年

该评论主要供其他人阅读,不要认为这是首选方式。我以为你知道自己在做什么。
Fredrik C

1

我一直在尝试性能,并希望做出两个贡献。

我的实验集中在DOM操作时间上。因此,在进行此操作之前,绝对值得遵循以上有关在创建可观察数组之前将其放入JS数组的观点,等等。

但是,如果仍然无法使用DOM操纵时间,那么这可能会有所帮助:


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

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

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

确保您可以加载微调器:

// 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/


1

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

在IE中打开开发人员工具会大大降低此操作的速度。我正在将〜1000个元素添加到数组中。当打开开发工具时,这大约需要10秒钟,并且IE冻结。当我关闭开发工具时,该操作是即时的,并且我看不到IE的运行速度降低。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.