记一次前端大量dom元素过滤优化

1,755 阅读2分钟

页面有一个文件列表, 在输入框里面输入文字的时候模糊匹配显示相应的文件名。

因为一次性会把所有文件列表都拿出来,所以做法是通过js控制文件列表的显示与否。开始做法很简单,通过js获取所有文件列表的dom元素,再获取文件名内容,通过比较,如果符合输入的文字则显示,不符合则隐藏。

这在小量文件的时候没问题,当文件到达一定数量的时候,比如说1万多个,这种方式几乎不可用,浏览器卡住时间太久。可以看下当时的性能分析数据:

从图中可以看到,花在js计算和页面渲染的时间非常多。 因为会去页面查找dom元素,通过dom元素hide或者show的时候都会引起页面的reflow,这都是比较耗时的。

所以,解决思路就是减少js计算时间和页面渲染时间。

  1. 减少js计算时间 肯定想到的是缓存,如果每次都去页面查询dom,为什么不第一次的时候把所有的dom元素都缓存起来呢,下一次在操作的时候就可以直接拿来用了。所以一开始的做法是把文件列表对应的id和文件名内容都缓存起来,这样每次查询的时候直接比对缓存的文件名,从而获得对应的dom元素id,就可以很简单使用js让对应id的dom元素隐藏或者显示了。但是这还是有一个问题,文件很多的时候,每个文件的隐藏或者显示都会引起页面的reflow,效果还是不好。
  2. 减少页面reflow/repaint时间 既然有这么多次的reflow/repaint 那能不能只发生一次呢?把所有需要显示的文件,预先放到内存里面组装好,一次性append到页面上。那不就只让页面只reflow/repaint 一次么。所以做法就是第一次操作的时候把所有的文件dom节点都clone出来,搜索的时候根据匹配规则得到匹配的dom节点,在内存里面把所有要显示的内容组装好,最后一次性append到页面上。大概的代码:
// 获取所有文件列表的dom节点信息以及文件名
var fileItems = $('.diff-file-item');
if(fileItems) {
   var files = Array.prototype.slice.call(fileItems);
   for(var i = 0; i < files.length; i++) {
       var fileItem = files[i];
       var fileId = $(fileItem).attr('id');
       if(fileId) {
           var fileName = $.trim($('.file-name', $(fileItem)).text()).toLowerCase();
           this.fileList.push({
               fileId: fileId,
               fileName: fileName,
               fileItem: $(fileItem).clone(),
           })
       }
   }
}

// 组装符合要求的列表页面
function filterFiles() {
    var terms = $(this).val();
    var tbody = $('<tbody></tbody>');
    for(var i = 0; i < self.fileList.length; i++) {
        var item = self.fileList[i];
        if(item.fileName.search(terms.toLowerCase()) !== -1) {
            tbody.append(item.fileItem);
        }
    }
    $('.diff-list').html(tbody);
}

可以看下最终的优化结果:

优化效果还是比较喜人的。