前言
我们在实际的工作中经常会遇到在一个页面插入几千甚至上万条数据。显然如果同时渲染上万条数据,页面肯定会卡住,这时候就需要我们对性能进行优化。
对于一次性插入大量数据,一般有两种做法:
1.时间分片
2.虚拟列表
本次我要介绍的是使用时间分片的方法去优化性能。
现在以在一个页面插入10000条数据为例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<ul id='contain'></ul>
</body>
</html>
粗暴的一次性渲染
强暴法插入100000条数据
let now = Date.now();
let Li = document.getElementById('contain')
const totalPage = 10000
for(let i =0;i<totalPage;i++){
let li = document.createElement('li');
li.innerText = Math.floor(Math.random()*totalPage)
Li.appendChild(li);
}
console.log(`js运行时间;${Date.now()-now}`)
setTimeout(()=>{
console.log(`总运行时间:${Date.now()-now}`)
},0)
粗暴的一次性渲染,会使页面十分卡顿,而且我记录了js的运行时间和页面渲染完成的时间
第一次console.log的是js运行时间200ms,第二次console.log的是页面渲染完成的时间3200ms。由此可见但渲染大量数据时,js的性能还是可以的,造成页面卡顿的是页面渲染阶段。
为什么第二次的是页面渲染完成的时间,这就有关于js运行机制的Event Loop了,我会在下篇文章详细讲解一下该机制。
使用定时器
有上面我们可以知道造成页面卡顿的市同时渲染大量的DOM,所以我们可以考虑将渲染过程分批进行,我们用定时器进行分批。这次使用的递归,每次渲染20条数据,分批进行
//使用的是setTimeout
let Li = document.getElementById('contain')
const totalPage = 10000
let once = 20
let index = 0
function loop(currentPage, currentIndex) {
if (currentPage <= 0) {
return false
}
let pageCount = Math.min(currentPage, once)
setTimeout(()=>{
for (let i = 0; i < pageCount; i++) {
let li = document.createElement('li');
li.innerText = Math.floor(Math.random() * totalPage)
Li.appendChild(li);
}
loop(currentPage-pageCount, currentIndex+pageCount) //
},0)
}
loop(totalPage, index)
显然,页面加载的速度已经非常快了,每次刷新时可以很快看到满屏的数据,但是但我们快速滚动页面时会出现闪屏或者白屏的现象。很显然我们还需要进行优化,那为什么会出现闪屏的现象呢?
闪屏现象分析
我们平时看到的连续页面其实是由一幅幅静止页面组成的,每幅画成为一帧。FPS表示的是每秒钟画面更新的次数。大多数电脑电脑显示器都是为60hz每秒。即你什么都不做时,大多显示器也会以每秒60次的频率不断的更新屏幕上的图像。
人为什么感觉不出来呢?
是由于眼睛有视觉停留效应,即前一幅画留在大脑的印象还没有消失,下一幅画又来了。使我们觉得页面是静止不动的。因此大多数的浏览器都会对重绘加以限制,大多不超过显示器的的重绘频率。因为即使超过那个频率,用户体验也不会提升。
由此我们可以得知,页面出现闪屏是由于每次渲染的速度过慢。这是由于setTimeout的执行时间是不确定的。在js中,setTimeout任务放到事件队列中,只有主线程执行完才会去检查事件队列是否有任务需要进行。因此setTimeout实际执行时间可能会比设定时间晚一些。因此这会导致setTimeout的执行步调和屏幕的刷新步调不一致,导致丢帧使屏幕卡顿。
使用requestAnimationFrame
与setTimeout相比,requestAnimationFrame最大的优势是由系统决定回调函数的执行时机。假如屏幕刷新率是60hz,那么回调函数隔16.7ms执行一次;假如屏幕刷新率是80hz,那么回调函数隔(1000/80=12.5ms)刷新一次,换句话说就是requestAnimationFrame的步伐跟着系统刷新的步伐走。能保证回调函数在屏幕每一次的刷新间隔只被执行一次,这样就不会引起丢帧现象。
let Li = document.getElementById('contain')
const totalPage = 10000
let once = 20
let index = 0
function loop(currentPage, currentIndex) {
if (currentPage <= 0) {
return false
}
let pageCount = Math.min(currentPage, once)
window.requestAnimationFrame(()=>{
for (let i = 0; i < pageCount; i++) {
let li = document.createElement('li');
li.innerText = Math.floor(Math.random() * totalPage)
Li.appendChild(li);
}
loop(currentPage-pageCount, currentIndex+pageCount)
})
}
loop(totalPage, index)
这样我们页面的加载速度快,并且在快速滚动的时候也没有出现闪烁丢帧的现象了。
使用DocumentFragment
不要以为这样优化完成了,还可以使用DocumentFragment。
DocumentFragment是DOM的节点,但并不是DOM树的一部分,存在内存中。当append元素到document中时,会同时计算样式表,而append元素到documentFragment时,不会计算元素的样式表,所以documentFragment性能更优。
最后修改代码如下:
function loop(currentPage, currentIndex) {
if (currentPage <= 0) {
return false
}
let pageCount = Math.min(currentPage, once)
window.requestAnimationFrame(()=>{
let fragment = document.createDocumentFragment()
for (let i = 0; i < pageCount; i++) {
let li = document.createElement('li');
li.innerText = Math.floor(Math.random() * totalPage)
fragment.appendChild(li);
}
Li.appendChild(fragment)
loop(currentPage-pageCount, currentIndex+pageCount)
})
}
loop(totalPage, index)