Knockout.js在半大型数据集下非常慢

我刚刚开始使用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每次我调用时都会进行一些重新计算,比如重build绑定的DOM对象。 有没有办法延迟这个rec​​alc,或者一次推入我的所有物品?

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

更新:

根据下面的build议,我已经更新了我的代码:

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

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

更新2:

根据下面的其他build议,我给jQuery.tmpl一个镜头(这本来是由KnockOut支持的),这个模板引擎将在3秒钟内绘制大约400行。 这似乎是最好的办法,缺乏一个解决scheme,可以dynamic加载更多的数据,当你滚动。

正如评论中所build议的那样。

Knockout拥有与(foreach,with)绑定关联的自己的本地模板引擎。 它还支持其他模板引擎,即jquery.tmpl。 在这里阅读更多的细节。 我没有做任何与不同引擎的基准,所以不知道是否会有所帮助。 阅读你以前的评论,在IE7中,你可能很难得到你以后的performance。

另外,KO支持任何js模板引擎,如果有人为它写了适配器的话。 您可能想尝试其他人,因为jquery tmpl将由JsRender取代。

请参阅: Knockout.js性能问题#2 – 操作observableArrays

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

除了使用$ .map之外,还要使用KO 分页

我有1400个logging的大型数据集同样的问题,直到我使用分页与淘汰赛。 使用$.map来加载logging确实有很大的不同,但是DOM渲染时间仍然是可怕的。 然后我尝试使用分页,这使得我的数据集照得很快,就像用户更友好一样。 页面大小为50使得数据集大大减less,显着减less了DOM元素的数量。

KO很容易做到:

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

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

给KoGrid一看。 它智能地pipe理你的行渲染,使它更高性能。

如果你试图使用foreach绑定将400行绑定到一个表,那么通过KO将这些内容推送到DOM中会遇到困难。

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

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

KoGrid完成这一切。 它的构build目的只是渲染查看器可以在页面上看到的行,然后虚拟化其他行直到需要它们。 我想你会发现400个项目的性能比你所遇到的要好得多。

在渲染一个非常大的数组时,避免locking浏览器的一个解决scheme是“限制”数组,使得一次只添加less量的元素,并在两者之间进行hibernate。 这是一个可以做到的function:

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

根据您的使用情况,这可能会导致大量的用户体验改进,因为用户在滚动之前只能看到第一批行。

利用push()接受可变参数给我的情况下performance最好。 1300行加载5973ms(〜6秒)。 通过这种优化,加载时间降低到914毫秒(<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); }; 

我一直在处理如此大量的数据, 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)后一次执行。

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

我的实验着重于DOM操作时间。 所以在进入这个之前,在创build一个可观察的数组之前,推入一个JS数组是非常值得的。

但是,如果DOM操作时间仍然在你的方式,那么这可能会有所帮助:


1:一个模式来包装一个加载微调包围慢速渲染,然后使用afterRender隐藏它

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

这不是一个真正解决性能问题的方法,但是如果你循环了上千个项目,并且它使用了一个模式,你可以确保你有一个加载微调器出现在长KO操作之前,那么延迟可能是不可避免的,之后。 所以它至less改善了用户体验。

确保你可以加载一个微调器:

 // 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绑定作为黑客

我记起了一个早期的技术,当时我正在使用Opera的机顶盒,使用DOM操作构buildUI。 这是令人震惊的缓慢,所以解决scheme是存储大块的HTML作为string,并通过设置innerHTML属性加载string。

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

下面是一个小提示,它展示了这种方法,还有一个可以从表格行内部调用的函数,以类似于KO的方式删除一个项目。 显然这不如正确的KO,但如果你真的需要炽烈的(ish)性能,这是一个可能的解决方法。

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

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

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

我也注意到,淘汰js模板引擎在IE中运行速度较慢,我用underscore.jsreplace它,工作方式更快。

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

在IE中打开开发人员工具会显着减慢此操作的速度。 我将〜1000个元素添加到数组中。 当开发工具打开时,这需要大约10秒钟,而IE正在发生冻结。 当我closures开发工具,操作是即时的,我看不到在IE中减速。