「前端进阶」高性能渲染十万条数据(时间分片)

前言

在实际工做中,咱们不多会遇到一次性须要向页面中插入大量数据的状况,可是为了丰富咱们的知识体系,咱们有必要了解并清楚当遇到大量数据时,如何才能在不卡主页面的状况下渲染数据,以及其中背后的原理。javascript

对于一次性插入大量数据的状况,通常有两种作法:html

  1. 时间分片
  2. 虚拟列表

本文做为开篇,着重来介绍如何使用时间分片的方式来渲染大量数据,虚拟列表相关的内容,参见「前端进阶」高性能渲染十万条数据(虚拟列表)前端

最粗暴的作法(一次性渲染)

咱们先来看看最粗暴的作法,一次性将大量数据插入到页面中:java

<ul id="container"></ul>
复制代码
// 记录任务开始时间
let now = Date.now();
// 插入十万条数据
const total = 100000;
// 获取容器
let ul = document.getElementById('container');
// 将数据插入容器中
for (let i = 0; i < total; i++) {
    let li = document.createElement('li');
    li.innerText = ~~(Math.random() * total)
    ul.appendChild(li);
}

console.log('JS运行时间:',Date.now() - now);
setTimeout(()=>{
  console.log('总运行时间:',Date.now() - now);
},0)
// print: JS运行时间: 187
// print: 总运行时间: 2844
复制代码

咱们对十万条记录进行循环操做,JS的运行时间为187ms,仍是蛮快的,可是最终渲染完成后的总时间确是2844msgit

简单说明一下,为什么两次console.log的结果时间差别巨大,而且是如何简单来统计JS运行时间总渲染时间github

  • 在 JS 的Event Loop中,当JS引擎所管理的执行栈中的事件以及全部微任务事件所有执行完后,才会触发渲染线程对页面进行渲染
  • 第一个console.log的触发时间是在页面进行渲染以前,此时获得的间隔时间为JS运行所须要的时间
  • 第二个console.log是放到 setTimeout 中的,它的触发时间是在渲染完成,在下一次Event Loop中执行的

关于Event Loop的详细内容请参见这篇文章-->数组

依照两次console.log的结果,能够得出结论:浏览器

对于大量数据渲染的时候,JS运算并非性能的瓶颈,性能的瓶颈主要在于渲染阶段微信

使用定时器

从上面的例子,咱们已经知道,页面的卡顿是因为同时渲染大量DOM所引发的,因此咱们考虑将渲染过程分批进行多线程

在这里,咱们使用setTimeout来实现分批渲染

<ul id="container"></ul>
复制代码
//须要插入的容器
let ul = document.getElementById('container');
// 插入十万条数据
let total = 100000;
// 一次插入 20 条
let once = 20;
//总页数
let page = total/once
//每条记录的索引
let index = 0;
//循环加载数据
function loop(curTotal,curIndex){
    if(curTotal <= 0){
        return false;
    }
    //每页多少条
    let pageCount = Math.min(curTotal , once);
    setTimeout(()=>{
        for(let i = 0; i < pageCount; i++){
            let li = document.createElement('li');
            li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
            ul.appendChild(li)
        }
        loop(curTotal - pageCount,curIndex + pageCount)
    },0)
}
loop(total,index);
复制代码

用一个gif图来看一下效果

咱们能够看到,页面加载的时间已经很是快了,每次刷新时能够很快的看到第一屏的全部数据,可是当咱们快速滚动页面的时候,会发现页面出现闪屏或白屏的现象

为何会出现闪屏现象呢

首先,理清一些概念。FPS表示的是每秒钟画面更新次数。咱们平时所看到的连续画面都是由一幅幅静止画面组成的,每幅画面称为一FPS是描述变化速度的物理量。

大多数电脑显示器的刷新频率是60Hz,大概至关于每秒钟重绘60次,FPS为60frame/s,为这个值的设定受屏幕分辨率、屏幕尺寸和显卡的影响。

所以,当你对着电脑屏幕什么也不作的状况下,大多显示器也会以每秒60次的频率正在不断的更新屏幕上的图像。

为何你感受不到这个变化?

那是由于人的眼睛有视觉停留效应,即前一副画面留在大脑的印象还没消失,紧接着后一副画面就跟上来了, 这中间只间隔了16.7ms(1000/60≈16.7),因此会让你误觉得屏幕上的图像是静止不动的。

而屏幕给你的这种感受是对的,试想一下,若是刷新频率变成1次/秒,屏幕上的图像就会出现严重的闪烁, 这样就很容易引发眼睛疲劳、酸痛和头晕目眩等症状。

大多数浏览器都会对重绘操做加以限制,不超过显示器的重绘频率,由于即便超过那个频率用户体验也不会有提高。 所以,最平滑动画的最佳循环间隔是1000ms/60,约等于16.6ms。

直观感觉,不一样帧率的体验:

  • 帧率可以达到 50 ~ 60 FPS 的动画将会至关流畅,让人倍感温馨;
  • 帧率在 30 ~ 50 FPS 之间的动画,因各人敏感程度不一样,温馨度因人而异;
  • 帧率在 30 FPS 如下的动画,让人感受到明显的卡顿和不适感;
  • 帧率波动很大的动画,亦会令人感受到卡顿。

简单聊一下 setTimeout 和闪屏现象

  • setTimeout的执行时间并非肯定的。在JS中,setTimeout任务被放进事件队列中,只有主线程执行完才会去检查事件队列中的任务是否须要执行,所以setTimeout的实际执行时间可能会比其设定的时间晚一些。
  • 刷新频率受屏幕分辨率和屏幕尺寸的影响,所以不一样设备的刷新频率可能会不一样,而setTimeout只能设置一个固定时间间隔,这个时间不必定和屏幕的刷新时间相同。

以上两种状况都会致使setTimeout的执行步调和屏幕的刷新步调不一致。

setTimeout中对dom进行操做,必需要等到屏幕下次绘制时才能更新到屏幕上,若是二者步调不一致,就可能致使中间某一帧的操做被跨越过去,而直接更新下一帧的元素,从而致使丢帧现象。

使用 requestAnimationFrame

setTimeout相比,requestAnimationFrame最大的优点是由系统来决定回调函数的执行时机。

若是屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次,若是刷新率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms,换句话说就是,requestAnimationFrame的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引发丢帧现象。

咱们使用requestAnimationFrame来进行分批渲染:

<ul id="container"></ul>
复制代码
//须要插入的容器
let ul = document.getElementById('container');
// 插入十万条数据
let total = 100000;
// 一次插入 20 条
let once = 20;
//总页数
let page = total/once
//每条记录的索引
let index = 0;
//循环加载数据
function loop(curTotal,curIndex){
    if(curTotal <= 0){
        return false;
    }
    //每页多少条
    let pageCount = Math.min(curTotal , once);
    window.requestAnimationFrame(function(){
        for(let i = 0; i < pageCount; i++){
            let li = document.createElement('li');
            li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
            ul.appendChild(li)
        }
        loop(curTotal - pageCount,curIndex + pageCount)
    })
}
loop(total,index);
复制代码

看下效果

咱们能够看到,页面加载的速度很快,而且滚动的时候,也很流畅没有出现闪烁丢帧的现象。

这就结束了么,还能够再优化么?

固然~~

使用 DocumentFragment

先解释一下什么是 DocumentFragment ,文献引用自MDN

DocumentFragment,文档片断接口,表示一个没有父级文件的最小文档对象。它被做为一个轻量版的Document使用,用于存储已排好版的或还没有打理好格式的XML片断。最大的区别是由于DocumentFragment不是真实DOM树的一部分,它的变化不会触发DOM树的(从新渲染) ,且不会致使性能等问题。
可使用document.createDocumentFragment方法或者构造函数来建立一个空的DocumentFragment

从MDN的说明中,咱们得知DocumentFragments是DOM节点,但并非DOM树的一部分,能够认为是存在内存中的,因此将子元素插入到文档片断时不会引发页面回流。

append元素到document中时,被append进去的元素的样式表的计算是同步发生的,此时调用 getComputedStyle 能够获得样式的计算值。 而append元素到documentFragment 中时,是不会计算元素的样式表,因此documentFragment 性能更优。固然如今浏览器的优化已经作的很好了, 当append元素到document中后,没有访问 getComputedStyle 之类的方法时,现代浏览器也能够把样式表的计算推迟到脚本执行以后。

最后修改代码以下:

<ul id="container"></ul>
复制代码
//须要插入的容器
let ul = document.getElementById('container');
// 插入十万条数据
let total = 100000;
// 一次插入 20 条
let once = 20;
//总页数
let page = total/once
//每条记录的索引
let index = 0;
//循环加载数据
function loop(curTotal,curIndex){
    if(curTotal <= 0){
        return false;
    }
    //每页多少条
    let pageCount = Math.min(curTotal , once);
    window.requestAnimationFrame(function(){
        let fragment = document.createDocumentFragment();
        for(let i = 0; i < pageCount; i++){
            let li = document.createElement('li');
            li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
            fragment.appendChild(li)
        }
        ul.appendChild(fragment)
        loop(curTotal - pageCount,curIndex + pageCount)
    })
}
loop(total,index);
复制代码

最后

本文更多的是提供一个思路,经过时间分片的方式来同时加载大量简单DOM。对于复杂DOM的状况,通常会用到虚拟列表的方式来实现,关于这一问题,会持续整理,敬请期待。

系列文章推荐

参考

写在最后

  • 文中若有错误,欢迎在评论区指正,若是这篇文章帮到了你,欢迎点赞关注
  • 本文同步首发与github,可在github中找到更多精品文章,欢迎Watch & Star ★
  • 后续文章参见:计划

欢迎关注微信公众号【前端小黑屋】,每周1-3篇精品优质文章推送,助你走上进阶之旅

同时欢迎加我好友,回复加群,拉你入群,和我一块儿学前端~

相关文章
相关标签/搜索