编程技术文章分享与教程

网站首页 > 技术文章 正文

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

hmc789 2024-11-11 12:50:13 技术文章 1 ℃


作者:吾名云中桥

转发链接:https://mp.weixin.qq.com/s/-UHOsR26jnnogHqPfPM0rQ

前言

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

对于一次性插入大量数据的情况,一般有两种做法:

「前端进阶」高性能渲染十万条数据(虚拟列表)

【前端进阶】高性能渲染十万条数据(时间分片) 本篇

本文作为开篇,着重来介绍如何使用 时间分片的方式来渲染大量数据。

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

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

<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,还是蛮快的,但是最终渲染完成后的总时间是 2844ms。

简单说明一下,为何两次 console.log的结果时间差异巨大,并且是如何简单来统计 JS运行时间和 总渲染时间:

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

关于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的情况,一般会用到虚拟列表的方式来实现,关于这一问题,会持续整理,敬请期待。

推荐JavaScript经典实例学习资料文章

「前端进阶」高性能渲染十万条数据(虚拟列表)

图解 Promise 实现原理(一):基础实现

图解 Promise 实现原理(二):Promise 链式调用

图解 Promise 实现原理(三):Promise 原型方法实现

图解 Promise 实现原理(四):Promise 静态方法实现

实践教你从零构建前端 Lint 工作流「干货」

高性能多级多选级联组件开发「JS篇」

深入浅出讲解Node.js CLI 工具最佳实战

延迟加载图像以提高Web网站性能的五种方法「实践」

比较 JavaScript 对象的四种方式「实践」

使用Service Worker让你的 Web 应用如虎添翼(上)「干货」

使用Service Worker让你的 Web 应用如虎添翼(中)「干货」

使用Service Worker让你的 Web 应用如虎添翼(下)「干货」

前端如何一次性处理10万条数据「进阶篇」

推荐三款正则可视化工具「JS篇」

如何让用户选择是否离开当前页面?「JS篇」

JavaScript开发人员更喜欢Deno的五大原因

仅用18行JavaScript实现一个倒数计时器

图文细说JavaScript 的运行机制

一个轻量级 JavaScript 全文搜索库,轻松实现站内离线搜索

推荐Web程序员常用的15个源代码编辑器

10个实用的JS技巧「值得收藏」

细品269个JavaScript小函数,让你少加班熬夜(一)「值得收藏」

细品269个JavaScript小函数,让你少加班熬夜(二)「值得收藏」

细品269个JavaScript小函数,让你少加班熬夜(三)「值得收藏」

细品269个JavaScript小函数,让你少加班熬夜(四)「值得收藏」

细品269个JavaScript小函数,让你少加班熬夜(五)「值得收藏」

细品269个JavaScript小函数,让你少加班熬夜(六)「值得收藏」

深入JavaScript教你内存泄漏如何防范

手把手教你7个有趣的JavaScript 项目-上「附源码」

手把手教你7个有趣的JavaScript 项目-下「附源码」

JavaScript 使用 mediaDevices API 访问摄像头自拍

手把手教你前端代码如何做错误上报「JS篇」

一文让你彻底搞懂移动前端和Web 前端区别在哪里

63个JavaScript 正则大礼包「值得收藏」

提高你的 JavaScript 技能10 个问答题

JavaScript图表库的5个首选

一文彻底搞懂JavaScript 中Object.freeze与Object.seal的用法

可视化的 JS:动态图演示 - 事件循环 Event Loop的过程

教你如何用动态规划和贪心算法实现前端瀑布流布局「实践」

可视化的 js:动态图演示 Promises & Async/Await 的过程

原生JS封装拖动验证滑块你会吗?「实践」

如何实现高性能的在线 PDF 预览

细说使用字体库加密数据-仿58同城

Node.js要完了吗?

Pug 3.0.0正式发布,不再支持 Node.js 6/8

纯JS手写轮播图(代码逻辑清晰,通俗易懂)

JavaScript 20 年 中文版之创立标准

值得收藏的前端常用60余种工具方法「JS篇」

箭头函数和常规函数之间的 5 个区别

通过发布/订阅的设计模式搞懂 Node.js 核心模块 Events

「前端篇」不再为正则烦恼

「速围」Node.js V14.3.0 发布支持顶级 Await 和 REPL 增强功能

深入细品浏览器原理「流程图」

JavaScript 已进入第三个时代,未来将何去何从?

前端上传前预览文件 image、text、json、video、audio「实践」

深入细品 EventLoop 和浏览器渲染、帧动画、空闲回调的关系

推荐13个有用的JavaScript数组技巧「值得收藏」

前端必备基础知识:window.location 详解

不要再依赖CommonJS了

犀牛书作者:最该忘记的JavaScript特性

36个工作中常用的JavaScript函数片段「值得收藏」

Node + H5 实现大文件分片上传、断点续传

一文了解文件上传全过程(1.8w字深度解析)「前端进阶必备」

【实践总结】关于小程序挣脱枷锁实现批量上传

手把手教你前端的各种文件上传攻略和大文件断点续传

字节跳动面试官:请你实现一个大文件上传和断点续传

谈谈前端关于文件上传下载那些事【实践】

手把手教你如何编写一个前端图片压缩、方向纠正、预览、上传插件

最全的 JavaScript 模块化方案和工具

「前端进阶」JS中的内存管理

JavaScript正则深入以及10个非常有意思的正则实战

前端面试者经常忽视的一道JavaScript 面试题

一行JS代码实现一个简单的模板字符串替换「实践」

JS代码是如何被压缩的「前端高级进阶」

前端开发规范:命名规范、html规范、css规范、js规范

【规范篇】前端团队代码规范最佳实践

100个原生JavaScript代码片段知识点详细汇总【实践】

关于前端174道 JavaScript知识点汇总(一)

关于前端174道 JavaScript知识点汇总(二)

关于前端174道 JavaScript知识点汇总(三)

几个非常有意思的javascript知识点总结【实践】

都2020年了,你还不会JavaScript 装饰器?

JavaScript实现图片合成下载

70个JavaScript知识点详细总结(上)【实践】

70个JavaScript知识点详细总结(下)【实践】

开源了一个 JavaScript 版敏感词过滤库

送你 43 道 JavaScript 面试题

3个很棒的小众JavaScript库,你值得拥有

手把手教你深入巩固JavaScript知识体系【思维导图】

推荐7个很棒的JavaScript产品步骤引导库

Echa哥教你彻底弄懂 JavaScript 执行机制

一个合格的中级前端工程师需要掌握的 28 个 JavaScript 技巧

深入解析高频项目中运用到的知识点汇总【JS篇】

JavaScript 工具函数大全【新】

从JavaScript中看设计模式(总结)

身份证号码的正则表达式及验证详解(JavaScript,Regex)

浏览器中实现JavaScript计时器的4种创新方式

Three.js 动效方案

手把手教你常用的59个JS类方法

127个常用的JS代码片段,每段代码花30秒就能看懂-【上】

深入浅出讲解 js 深拷贝 vs 浅拷贝

手把手教你JS开发H5游戏【消灭星星】

深入浅出讲解JS中this/apply/call/bind巧妙用法【实践】

手把手教你全方位解读JS中this真正含义【实践】

书到用时方恨少,一大波JS开发工具函数来了

干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)

手把手教你JS 异步编程六种方案【实践】

让你减少加班的15条高效JS技巧知识点汇总【实践】

手把手教你JS开发H5游戏【黄金矿工】

手把手教你JS实现监控浏览器上下左右滚动

JS 经典实例知识点整理汇总【实践】

2.6万字JS干货分享,带你领略前端魅力【基础篇】

2.6万字JS干货分享,带你领略前端魅力【实践篇】

简单几步让你的 JS 写得更漂亮

恭喜你获得治疗JS this的详细药方

谈谈前端关于文件上传下载那些事【实践】

面试中教你绕过关于 JavaScript 作用域的 5 个坑

Jquery插件(常用的插件库)

【JS】如何防止重复发送ajax请求

JavaScript+Canvas实现自定义画板

Continuation 在 JS 中的应用「前端篇」

作者:吾名云中桥

转发链接:https://mp.weixin.qq.com/s/-UHOsR26jnnogHqPfPM0rQ

Tags:

标签列表
最新留言