页面中长列表滚动的优化

蚊子前端博客
发布于 2019-04-23 18:10
当我们有10万条或者更多的数据需要展示在页面中,会出现哪些问题呢,我们应该怎样处理和优化呢?

当我们有10万条或者更多的数据需要展示在页面中,我们应该怎样处理呢?

这里主要的方式有两种:

  1. 懒加载:即监听scroll事件或使用IntersecionObserver监听;

  2. 可视区域的渲染:仅在可视区域展示数据,为保证滚动条的完整性,非可视区域使用占位元素的高度后者容器的位移来撑开。

1. 懒加载

懒加载的方式有两种:监听scroll事件或使用IntersecionObserver监听某个元素。

1.1 scroll事件

监听滚动事件应该是我们使用的最多的事件了,当滚动条滑动到页面最底部或者将要滑动到最底部的时候,去加载下一页的数据。同时图片懒加载或者其他组件的懒加载也可以依赖于滚动事件!

为了优化滚动事件,我们也会给滚动事件添加上防抖和节流

这是一个很常见的demo,无限加载数据系列,当滚动条滑动到底部时,加载下一页的数据:监听滚动事件懒加载数据

请分别用微信和QQ扫下这个二维码体验一下,体验时,请点击“向左”的按钮关闭代码的部分:

监听滚动事件懒加载数据-蚊子博客

其实我们发现会有一个很有意思的体验:在android中的微信和QQ中基本可以无限的滑动,在iOS的微信中也可以,但是在iOS的QQ中,当我们一直滑动时,需要滑动到底部,然后才能加载新的数据,才能继续滑动!

这种原因是iOS不同的webview造成的。在iOS中,UIWebView是在iOS2中出现的,性能更好的WKWebView跟随iOS8一起出现。从性能方面来说,WKWebView会比UIWebView高很多,可以算是一次飞跃。它采用了跨进程的方案,用 Nitro JS 解析器,高达 60fps 的刷新率。同时,提供了很好的H5页面支持,类比UIWebView还多提供了一个加载进度的属性。同时,在UIWebView中,无论是页面的body滚动,还是div级别的局部滚动,都不会实时触发,而是在滚动结束后才会触发!

可以分别体验下面的两个二维码,左侧的为body级别的滚动,右侧为div的局部滚动:

body级别的滚动-蚊子博客 div的局部滚动-蚊子博客

总结下滚动事件在不同的webview中的表现情况:

机型body滚动局部滚动
iOS WKWebView实时触发实时触发
iOS UIWebView无法实时触发无法实时触发
Android实时触发实时触发

那么该如何优化滚动的性能呢?

首先就是防抖和节流。在Chrome浏览器中使用performance.now()测试滚动每次触发的间隔,可以看到,频率大概在16ms左右,但很多时候我们并不需要这么快地触发我们的监听函数,而且,比如图片懒加载等场景,也要及时的更新一次,这时候就用到了防抖和节流!

再有就是使用requestAnimationFramerequestIdleCallback代替定时器。若使用定时器进行间歇性触发监听函数,会出现掉帧的情况,我们也知道setTimeout(fn, 100),不一定是正好在100ms后触发fn函数,而是等浏览器空闲之后才调用fn函数,当多次积累后就会产生掉帧的,那么使用requestAnimationFramerequestIdleCallback,则浏览器会根据自己渲染的频率适时地执行回调。在追求高性能的渲染效果时,可以考虑用requestIdleCallback()和requestAnimationFrame()代替定时器。前者适合流畅的动画效果场景,后者适用于分离一些优先级低的操作逻辑,使用时需要考虑清楚

1.2 IntersecionObserver

使用IntersecionObserver也可以实现无限滚动,比如在底部监听一个透明的元素,当该元素可见时就加载新的资源!

demo: IntersecionObserver实现无限滚动

不过也应当看到的是IntersecionObserver的支持程度还不太好,如果要使用的话,还是需要polyfill方案!

IntersecionObserver的浏览器支持程度-蚊子的前端博客

而且IntersecionObserver在图片懒加载和组件懒加载的过程中,非常有用,我们后续会进行了解!

2. 模拟滚动

模拟滚动最代表的例子就是iScroll,监听手势的touchmove事件,然后使用CSS3中的transform产生位移。

不过在模拟滚动的过程中,若图片比较多时,可以感觉到滚动时有明显的卡顿感。查看这个样例:https://www.xiabingbao.com/demos/20190423/iscroll-scroll.html

这是利用IScroll中的scroll事件,无限地向页面中添加元素。不过当页面的数据是无限加载时,应当使用iscroll-infinite这个类库更好,这部分留到后面讲解!

3. 可视区域数据的渲染

上面的几种方法里,我们都是无限地直接往页面中添加元素,滑动的页码越大,页面中的DOM元素也就越多。但实际上,这是没有必要的,滚动到可视区域外的元素,用户是看不到的,也没不要保留在页面中,可以用一个占位元素或者transform撑开上面所有不可见元素的高度,让滚动条能够正常的上下滑动即可!

撑开上半部分不可见区域的高度,有两种方法,一种是在容器内的最顶部设置一个占位元素,这个占位元素的高度就是消失的所有DOM的高度;再一个就是给容器或者占位元素一个transform的位移!这里我们使用给占位元素一个高度撑起顶部的滚动区域。

3.1 固定高度的item

若每个item的height/margin/padding等数据都是固定写死的,这种情况比较容易实现!获取一次数据后,不用每次都重新计算。

每次滚动时,都要计算应当渲染列表的哪部分!start表示列表开始的位置,fixedScrollTop表示顶部占位元素的距离

COPYJAVASCRIPT

// 滚动处理函数 function handleScroller() { let lastStart = 0; // 上次开始的位置 const item = document.querySelector('.container .item'); const itemStyle = getComputedStyle(item); const itemHeight = item.offsetHeight + parseInt(itemStyle['marginTop']) + parseInt(itemStyle['marginBottom']); return function() { const currentScrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop); const fixedScrollTop = currentScrollTop - currentScrollTop % itemHeight; let start = Math.floor( currentScrollTop/itemHeight ); // 可视区域开始渲染的位置 if (lastStart!==start) { lastStart = start; createDom(start, count, fixedScrollTop); } } }

设置顶部滚动的高度:

COPYJAVASCRIPT

function createDom(start, count, height) { const container = document.querySelector('.container'); // container.style.transform = `translateY(${height}px)`; // 给容易一个位移 let div = document.createDocumentFragment(); // 创建占位元素 if (height) { let p = document.createElement('p'); p.style.height = height + 'px'; div.appendChild(p); } for(let i=start, len=start+count; i<len; i++) { let item = document.createElement('div'); item.className = 'item'; item.innerHTML = i; div.appendChild(item); } // 为了方便处理,我们这里采用了更新container中的全部元素 // 你也可以尝试只增加/删除首位的元素,中间元素不变 container.innerHTML = ''; container.appendChild(div); }

可以点击链接查看样例:可视区域渲染之DOM元素增减,审查元素可以看得更清晰,无论我们怎么滚动,.container中永远只有那么几个元素!

3.2 每个item的高度都不一定

待定,有个问题一直搞不定

3.3 DOM的复用

重点来了,在上面的章节里,我们进行了进一步的优化,只渲染可视区域内的数据。但是还是存在一个重要的问题:频繁的改动DOM,这样会频繁的引起页面的重绘。这里我们的思想是对页面中DOM元素进行复用,如下图中所示,从上面滑出的元素,可以直接定位下面再重新装填元素,反之亦然!

DOM元素的复用-蚊子博客-蚊子博客-蚊子的前端博客

可以点击链接查看demo,在审查元素中我们可以看到,在滚动的过程中,DOM并没有被删掉,而是改变了transform,放到了最下面:可视区域渲染之DOM元素复用

COPYJAVASCRIPT

// 更新页面中DOM元素的位置 function updateDom(start, count, itemHeight, height) { document.querySelector('.container .content').style.transform = 'translateY('+height+'px)'; for (var i = start, len=start+count; i < len; i++) { var index = i % count; var cssIndex = (i-start) % len; document.querySelector('.item' + index).innerHTML = i; document.querySelector('.item' + index).style.transform = 'translateY('+itemHeight*cssIndex+'px)'; } }

这里我们还是监听了元素的scroll事件,我们在上面一笔带过的iScroll-infinite类库,是使用了模拟滚动+DOM复用。点击链接查看demo:iScroll-infinite实现的无限加载

4. 总结

最有效的方案应当是模拟滚动+DOM复用,既解决了滚动不能实时生效的问题,又能解决页面中DOM元素过多,造成滚动中卡顿的问题。

总是想着要讲的有很多,不过最后好像哪方面都欠缺了一点,后面有机会再针对其中的某一个点再深入进行探讨!例如使用IntersecionObserver实现Vue组件的懒加载,如何自己实现一个简单的模拟滚动等。

参考:

http://taobaofed.org/blog/2017/03/02/thinking-in-request-animation-frame

http://wiki.jikexueyuan.com/project/iscroll-5/customevents.html

标签:
阅读(4645) 评论(2)

公众号:

qrcode

微信公众号:前端小茶馆