前端的长列表性能优化

2017-02-27 fishedee 前端

1 概述

前端的长列表性能优化

2 问题

最近在体验小程序时,发现小程序的体验是惊讶的好。这使得我一度以为小程序就是react-native的实现方式,将代码映射成原生的控件来渲染页面的。后来,我在小程序的帮助文档中发现,原来小程序仅仅就是个web的实现而已。

可是,我在使用大众点评网的小程序时,遇到了web常见的超长列表显示时,小程序竟然一点都不卡。甚至比react-native还要好,react-native在多图的超长列表中就直接崩溃了。

那么问题来了,为什么同样是浏览器的页面渲染,我们用html5开发的长列表这么卡,而小程序却不卡。

3 原理

在开发android,oc等桌面程序时,一个针对长列表的重要优化是,只渲染页面中用户能看到的部分。例如一个列表超过5000条,但是当中呈现给用户看到中的一屏显示就只有10条。毕竟屏幕有限,你不可能一屏显示完所有列表条目,你需要不断翻页才能看完所有的列表条目。

因此,android中的ListView,oc中的TableView中都有复用控件的优化,并且在不断滚动的过程中去除不在屏幕中的元素,不再渲染,从而实现高性能的列表渲染。

借鉴着这个想法,我们思考一下。当列表不断往下拉时,web中的dom元素就越多,即使这些dom元素已经离开了这个屏幕,不被用户所看到了,这些dom元素依然存在在那里。导致浏览器在渲染时需要不断去考虑这些dom元素的存在,造成web浏览器的长列表渲染非常低效。因此,实现的做法就是捕捉scroll事件,当dom离开屏幕,用户不再看到时,就将其移出dom tree。

4 实现

<!doctype html5>
<html>
    <head>
        <meta charset="utf-8"/>
        <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
        <title>长列表优化测试</title>
        <script src="jquery.js"></script>
        <style>
            *{
                padding: 0px;
                margin:0px;
            }
            li{
                height:50px;
                line-height:50px;
                font-size:20px;
                color:red;
                background:blue;
                border-bottom:solid 1px yellow;
            }
        </style>
    </head>
    <body>
        <div>长列表测试</div>
        <ul>
        </ul>
        <script>
            var ul = $('ul');
            var newCounter = 1;
            var prefix = $('<li></li>');
            var liCache = [];

            prefix.css('border','0px');
            prefix.css('height','0px');
            ul.append(prefix);
            ul.append('<li>1</li>');

            function checkIsBottom(target){
                var winHeight = window.innerHeight;
                var scrollY = window.scrollY;
                var targetBottom = target.offset().top+target.height();
                return targetBottom > scrollY+winHeight;
            }

            function checkIsTop(target){
                var scrollY = window.scrollY;
                var targetBottom = target.offset().top+target.height();
                return targetBottom > scrollY;
            }

            function newLi(){
                var li;
                if( liCache.length == 0 ){
                    li  = $('<li></li>');
                    console.log('new Li,counter:'+newCounter);
                    newCounter++;
                }else{
                    li = liCache.pop();
                }
                return li;
            }

            function delLi(li){
                li.remove();
                liCache.push(li);
            }

            function delTopData(li){
                while(true){
                    //console.log(li.text());
                    var nextLi = li.next();
                    var height = li.height();
                    var prefixHeight = prefix.height();
                    delLi(li);
                    prefix.height(prefixHeight+height);
                    if( checkIsTop(nextLi) ){
                        break;
                    }
                    li = nextLi;
                }
            }

            function addBottomData(){
                var lastLi = $('li:last');
                var counter = parseInt(lastLi.text());
                while(true){
                    var li = newLi();
                    li.text(++counter);
                    ul.append(li);
                    if(checkIsBottom(li)){
                        break;
                    }
                }
            }

            function addTopData(){
                var prefixLi = $('li:first');
                var firstLi = $('li:first').next();
                while(true){
                    var newFirstLi = newLi();
                    var prefixHeight = prefix.height();
                    newFirstLi.text(parseInt(firstLi.text())-1);
                    firstLi.before(newFirstLi);
                    prefix.height(prefixHeight-newFirstLi.height());
                    if( prefixLi.height() == 0 || checkIsTop(prefixLi) == false ){
                        break;
                    }
                    firstLi = newFirstLi;
                }
            }

            function delBottomData(){
                var prefixLi = $('li:first');
                var li = $('li:last');
                var prevLi = li.prev();
                while(true){
                    delLi(li);
                    li = prevLi;
                    prevLi = li.prev();
                    if( prevLi.prev()[0] == prefixLi[0] || checkIsBottom(prevLi) == false){
                        break;
                    }
                }
            }

            addBottomData();

            $(window).scroll(function(){
                //顶部移除节点
                var firstLi = $('li:first').next();
                if( checkIsTop(firstLi) == false){
                    delTopData(firstLi);
                }

                //尾部移除节点
                var suffixLi = $('li:last').prev();
                if( checkIsBottom(suffixLi)){
                    delBottomData();
                }

                //尾部添加节点
                var lastLi = $('li:last');
                if( checkIsBottom(lastLi) == false ){
                    addBottomData();
                }

                //顶部添加节点
                var prerfixLi = $('li:first');
                if( prerfixLi.height() != 0 && checkIsTop(prerfixLi) == true ){
                    addTopData();
                }               

            });
        </script>
    </body>
</html>

按着这个想法,我实现了以上的代码,你能看到的是,即使不断往下拉列表,dom tree中可以看到的li数量都是有限的,而且,从console中可以看到,li的new数量极小,其会不断复用那些离开屏幕了的dom,这让整个内存的使用控制在一个很低水平。

嗯,这个demo也存在一些没有实现的地方

  • 往上拉了以后,底部的列表条目没有留下来,全部都删了,导致下一次往下拉时又要重复拉数据。
  • 列表条目的高度都是固定的
  • 列表条目的数据没有存下来,重建数据时是用附近条目的数据来补充过来的

如果理解了上面demo的话,这三点其实也不难解决了。就这样,你就能实现属于自己的Web的ListView,从而在浏览器中得到与小程序一样的渲染性能了。

5 总结

挺有意思的,而且常见和重要的优化,我一直以为在Web中是实现不了,没想到也可以。同理,这个优化也可以用在react-native上,估计也能大幅减少内存的消耗和提高渲染性能。

不过我大概想了下,这种优化的局限在于,当列表条目的高度在变化时,你需要手动通知列表来通知这种变化,让列表来调整滚动栏位置和可见条目。这时候,对业务有较大侵入性,这种情况下就不太适用这种优化方式了。不过,话说,长列表的条目的高度中途会变化的事情我在正规的app上就从来没看过,除了那些图片没做好加载完成后自动拉伸高度的渣体验。

对了,Atom的文本编辑器与VSCode的文本编辑器都是html5实现,但Atom打开大文件时会卡出翔就是由于这个问题,看这里

相关文章