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打开大文件时会卡出翔就是由于这个问题,看这里。
- 本文作者: fishedee
- 版权声明: 本博客所有文章均采用 CC BY-NC-SA 3.0 CN 许可协议,转载必须注明出处!