JavaScript中动态创建大型HTML表格的性能

21
我有一个用于数据分析的应用程序,我在创建表格时遇到了一些性能问题。数据是从文档中提取的,在一页上呈现所有数据非常重要(不幸的是,无法使用分页)。
使用jQuery,我发出ajax请求来检索数据。请求完成后,我将数据传递给输出函数。输出函数使用for循环遍历数据数组并将行连接到变量中。循环完成后,包含表格的变量被附加到页面上的现有div上,然后我继续为表格绑定事件以处理数据。
对于小型数据集(~1000-2000行),它可以正常运行,但某些数据集包含超过10,000行,这会导致Firefox崩溃或变得无响应。
我的问题是,是否有更好的方法来完成我正在做的事情?
下面是一些代码:
//This function gets called by the interface with an id to retrieve a document
function loadDocument(id){
    $.ajax({
        method: "get",
        url: "ajax.php",
        data: {action:'loadDocument',id: id},
        dataType: 'json',
        cache: true,
        beforeSend: function(){
            if($("#loading").dialog('isOpen') != true){
                //Display the loading dialog
                $("#loading").dialog({
                    modal: true
                });
            }//end if
        },//end beforesend
        success: function(result){
            if(result.Error == undefined){
                outputDocument(result, id);
            }else{
                <handle error code>
            }//end if
            if($('#loading').dialog('isOpen') == true){
                //Close the loading dialog
                $("#loading").dialog('close');
            }//end if
        }//end success
    });//end ajax
};//end loadDocument();


//Output document to screen
function outputDocument(data, doc_id){

    //Begin document output
    var rows = '<table>';
    rows += '<thead>';
    rows += '<tr>';
    rows += '<th>ID</th>';
    rows += '<th>Status</th>';
    rows += '<th>Name</th>';
    rows += '<th>Actions</th>';
    rows += '<th>Origin</th>';
    rows += '</tr>';
    rows += '</thead>';
    rows += '<tbody>';

    for(var i in data){
        var recordId = data[i].id;
        rows += '<tr id="' + recordId + '" class="' + data[i].status + '">';
        rows += '<td width="1%" align="center">' + recordId + '</td>';
        rows += '<td width="1%" align="center"><span class="status" rel="' + recordId + '"><strong>' + data[i].status + '</strong></span></td>';
        rows += '<td width="70%"><span class="name">' + data[i].name + '</span></td>';
        rows += '<td width="2%">';
        rows += '<input type="button" class="failOne" rev="' + recordId + '" value="F">';
        rows += '<input type="button" class="promoteOne" rev="' + recordId + '" value="P">';
        rows += '</td>';
        rows += '<td width="1%">' + data[i].origin + '</td>';
        rows += '</tr>';
    }//end for

    rows += '</tbody>';
    rows += '</table>';
    $('#documentRows').html(rows);

我最初使用了 jQuery 的 each 循环,但后来改用 for 循环,这样可以减少一些毫秒。

我考虑使用类似 Google Gears 的东西来尝试卸载一些处理工作(如果在这种情况下可能的话)。

有什么想法?


展示前一百个,然后使用定时器或者工作线程来构建其余的。请记住浏览器是单线程的。当 JavaScript 在执行时,浏览器会变得不可响应。使用事件委托技术。 - jasssonpet
使用虚拟滚动 http://nexts.github.io/Clusterize.js/ - Denis
7个回答

22

你好,

渲染可能是一个问题,但在循环内连接这么多字符串也是个问题,特别是当字符串变得非常大时。最好将字符串放入数组的单独元素中,然后最后使用 "join" 一次性创建一个巨大的字符串。例如:

var r = new Array();
var j = -1, recordId;
r[++j] =  '<table><thead><tr><th>ID</th><th>Status</th><th>Name</th><th>Actions</th><th>Origin</th></tr></thead><tbody>'; 
for (var i in data){
    var d = data[i];
    recordId = d.id;
    r[++j] = '<tr id="';
    r[++j] = recordId;
    r[++j] = '" class="';
    r[++j] = d.status;
    r[++j] = '"><td width="1%" align="center">';
    r[++j] = recordId;
    r[++j] = '</td><td width="1%" align="center"><span class="status" rel="';
    r[++j] = recordId;
    r[++j] = '"><strong>';
    r[++j] = d.status;
    r[++j] = '</strong></span></td><td width="70%"><span class="name">';
    r[++j] = d.name;
    r[++j] = '</span></td><td width="2%"><input type="button" class="failOne" rev="';
    r[++j] = recordId;
    r[++j] = '" value="F"><input type="button" class="promoteOne" rev="';
    r[++j] = recordId;
    r[++j] = '" value="P"></td><td width="1%">';
    r[++j] = d.origin;
    r[++j] = '</td></tr>';
}
r[++j] = '</tbody></table>';
$('#documentRows').html(r.join(''));

此外,我建议使用这里展示的数组索引方法,而不是使用“push”,因为除了Google Chrome之外的所有浏览器都更快,根据这篇文章


1
当你构造数组时,可以通过指定数组的长度来提高性能,例如:var r = new Array(data.length*19+2),这样可以挤出更多的性能。 - Neil
此外,在表格的'<td>'标签中,您将宽度作为百分比包含在内。这是很多重复的规范。为什么不只在顶部的'<th>'元素上放置宽度值呢?'align="center"'规范也是类似的。这样,您就可以节省引擎查看每个单独数据单元格的宽度并减少字符串大小的时间。 - Neil
使用 ++j 很好,我建议修复循环定义以获得更好的性能。 - digiguru
Digiguru,谢谢。我来自于'C'和Perl编程背景,并且一直主张使用'++'、'--'前缀后缀运算符以及'+='、'-='、'*='运算符来提高代码的清晰度。 - Neil
3
我在一个虚拟分页表格中使用这个代码,发现不每次重新创建整个表格可以获得更好的性能。我只是设置tbody的HTML内容。这还有一个好处是数据加载时标题仍然可见。 - PeterBelm

4
显示这么多行导致浏览器的渲染引擎变慢,而不是JavaScript引擎。不幸的是,你没有太多可以做的。
最好的解决方案是不同时显示那么多行,通过分页或虚拟滚动来实现。

谢谢大家,我不确定虚拟滚动是否可行,但我想尝试按@jasssonpet的建议每次加载x条记录。我知道将记录附加到现有表格很昂贵,不过我想知道这是否只会将瓶颈移至那里? - fuel37

2
您正在构建字符串的方式将导致大量垃圾回收。
随着字符串越来越长,JavaScript 引擎不得不保持分配更大的缓冲区并丢弃旧的缓冲区。最终,如果不回收所有旧字符串的残留物,它将无法分配足够的内存。
随着字符串变得越来越长,这个问题会变得更加严重。
相反,尝试使用 jQuery 操作 API 逐个添加新元素到 DOM 中。
此外,请考虑仅呈现可见内容并实现自己的滚动。

你的回答的第一部分正是我所需要的。虽然我不太了解JavaScript引擎的内部机制,但我认为将所有数据附加到变量上肯定会导致某些问题。有没有更好的方法来完成这个任务而不必将数据附加到表格并重置变量呢?我尝试在每次循环中将行附加到表格中,但对于包含7200条记录的文档,时间从5000毫秒增加到了132000毫秒。 - fuel37
说实话,我认为最好的答案就是不显示那么多数据。很难想象我会想要滚动浏览10000行甚至1000行的情况。 - Noel Walters
对我来说也一样不太清楚!!我不想说这个应用程序是什么,但是数据需要按顺序和整体显示以便发现趋势等。我正在尝试想出某种解决方案来避免这种情况,但它很视觉化并且难以解释,你必须亲眼看到:)。非常感谢您的回复,它让我有了新的方向! - fuel37
2
你尝试过在将表格插入DOM之前先在内存中构建整个表格吗?这样可以避免浏览器每次添加行时都需要重新流式布局和重绘。 - Martijn
使用jQuery操作API构建一个巨大的表格确实非常慢...如果他追求性能,那么字符串是更好的选择(虽然不够优雅)。 - Jeffrey Kevin Pry

1

您可以采取一些措施来提高性能:

  1. 您的变量越来越大,因此不要将html存储在一个变量中。解决办法可以是使用$.each()函数,在每个函数中将元素附加到DOM中。但这只是小的调整。
  2. 生成Html很好,但您可以尝试创建和附加DOM。像$('<tr></tr>')
  3. 最后,这肯定会解决您的问题:在第一个ajax调用中使用多个ajax调用,收集有多少数据可用,并获取大约1,000或更多的数据。并使用其他调用来收集剩余数据。如果需要,可以明智地使用同步调用或异步调用。

但尽量避免存储值。您的DOM大小将非常巨大,但它应该适用于现代浏览器,请忘记IE6。

@fuel37:示例

function outputDocumentNew(data, doc_id) {
    //Variable DOM's
    var rowSample = $('<tr></tr>').addClass('row-class');
    var colSample = $('<td></td>').addClass('col-class');
    var spanSample = $('<span></span>').addClass('span-class');
    var inputButtonSample = $('<input type="button"/>').addClass('input-class');

    //DOM Container 
    var container = $('#documentRows');
    container.empty().append('<table></table>');

    //Static part
    var head = '<thead>\
                <tr>\
                    <th width="1%" align="center">ID</th>\
                    <th width="1%" align="center">Status</th>\
                    <th width="70%">Name</th>\
                    <th width="2%">Actions</th>\
                    <th width="1%">Origin</th>\
                </tr>\
                </thead>';
    container.append(head);

    var body = $('<tbody></tbody>');
    container.append(body);

    //Dynamic part
    $.each(data, function (index, value) {
        var _this = this;

        //DOM Manupulation
        var row = rowSample.clone();

        //Actions
        var inpFailOne = inputButtonSample.clone().val('F').attr('rev', _this.id).addClass('failOne').click(function (e) {
            //do something when click the button.
        });
        var inpPromoteOne = inputButtonSample.clone().val('P').attr('rev', _this.id).addClass('promoteOne').click(function (e) {
            //do something when click the button.
        });

        row
        .append(colSample.clone().append(_this.id))
        .append(colSample.clone().append(spanSample.colne().addClass('status').append(_this.status)))
        .append(colSample.clone().append(spanSample.colne().addClass('name').append(_this.name)))
        .append(colSample.clone().append(inpFailOne).append(inpPromoteOne))
        .append(colSample.clone().append(_this.origin));

        body.append(row);
    });
}

在这个过程中,您需要创建和维护用于操作的ID或类。您可以控制绑定事件并操纵每个元素。

你能提供一个#2的例子吗?我以前从未以这种方式创建过元素。 - fuel37

0

回答以获取格式

如果你这样做会发生什么

for(var i in data){
    var record = data[i];
    var recordId = record.id;
    rows += '<tr id="' + recordId + '" class="' + record.status + '">';
    rows += '<td width="1%" align="center">' + recordId + '</td>';
    rows += '<td width="1%" align="center"><span class="status" rel="' + recordId + '"><strong>' + data[i].status + '</strong></span></td>';
    rows += '<td width="70%"><span class="name">' + record.name + '</span></td>';
    rows += '<td width="2%">';
    rows += '<input type="button" class="failOne" rev="' + recordId + '" value="F">';
    rows += '<input type="button" class="promoteOne" rev="' + recordId + '" value="P">';
    rows += '</td>';
    rows += '<td width="1%">' + record.origin + '</td>';
    rows += '</tr>';
}//end for

然后我同意Box9关于分页建议的看法。 - mplungjan
1
有那么多的数据,我建议使用某种类型的字符串缓冲区。否则程序速度会变得非常慢 - 请参阅 http://www.softwaresecretweapons.com/jspwiki/javascriptstringconcatenation 以获得更多详细信息。 - kikito

0

可以试试这个...

改进循环

改进字符串拼接

var tmpLst = [];

for (var i=0, il=data.length; i<il; i++) {
    var record = data[i];
    var recordId = record.id;

    tmpLst.push('<tr id="');
    tmpLst.push(recordId); 
    tmpLst.push('" class="'); 
    tmpLst.push(record.status);
    tmpLst.push('">');
    tmpLst.push('<td width="1%" align="center">');

...ect...


}
rows += tmpLst.join('');

这可能会挤出额外的性能...

var lstReset = i * lstReset.length;
tmpLst[lstReset + 1]='<tr id="';
tmpLst[lstReset + 2]=recordId; 
tmpLst[lstReset + 3]='" class="'; 

0

根据其他人的建议(我还不够有声望来发表评论,抱歉!),您可以尝试TableSorter插件来处理一次只显示可用的数据量。

我不知道它在非常高数量的行上的表现如何,但他们的示例数据大约为1000行。

这不会帮助JS性能,但会减轻浏览器渲染器的负担。


我在另一个实例中尝试过这个,大约在2000条记录后似乎也会出现问题。 - fuel37

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接