网站首页 > 技术文章 正文
作者:吾名云中桥
转发链接: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 静态方法实现》
《使用Service Worker让你的 Web 应用如虎添翼(上)「干货」》
《使用Service Worker让你的 Web 应用如虎添翼(中)「干货」》
《使用Service Worker让你的 Web 应用如虎添翼(下)「干货」》
《一个轻量级 JavaScript 全文搜索库,轻松实现站内离线搜索》
《细品269个JavaScript小函数,让你少加班熬夜(一)「值得收藏」》
《细品269个JavaScript小函数,让你少加班熬夜(二)「值得收藏」》
《细品269个JavaScript小函数,让你少加班熬夜(三)「值得收藏」》
《细品269个JavaScript小函数,让你少加班熬夜(四)「值得收藏」》
《细品269个JavaScript小函数,让你少加班熬夜(五)「值得收藏」》
《细品269个JavaScript小函数,让你少加班熬夜(六)「值得收藏」》
《手把手教你7个有趣的JavaScript 项目-上「附源码」》
《手把手教你7个有趣的JavaScript 项目-下「附源码」》
《JavaScript 使用 mediaDevices API 访问摄像头自拍》
《一文彻底搞懂JavaScript 中Object.freeze与Object.seal的用法》
《可视化的 JS:动态图演示 - 事件循环 Event Loop的过程》
《可视化的 js:动态图演示 Promises & Async/Await 的过程》
《Pug 3.0.0正式发布,不再支持 Node.js 6/8》
《通过发布/订阅的设计模式搞懂 Node.js 核心模块 Events》
《「速围」Node.js V14.3.0 发布支持顶级 Await 和 REPL 增强功能》
《JavaScript 已进入第三个时代,未来将何去何从?》
《前端上传前预览文件 image、text、json、video、audio「实践」》
《深入细品 EventLoop 和浏览器渲染、帧动画、空闲回调的关系》
《推荐13个有用的JavaScript数组技巧「值得收藏」》
《36个工作中常用的JavaScript函数片段「值得收藏」》
《一文了解文件上传全过程(1.8w字深度解析)「前端进阶必备」》
《手把手教你如何编写一个前端图片压缩、方向纠正、预览、上传插件》
《JavaScript正则深入以及10个非常有意思的正则实战》
《前端开发规范:命名规范、html规范、css规范、js规范》
《100个原生JavaScript代码片段知识点详细汇总【实践】》
《手把手教你深入巩固JavaScript知识体系【思维导图】》
《一个合格的中级前端工程师需要掌握的 28 个 JavaScript 技巧》
《身份证号码的正则表达式及验证详解(JavaScript,Regex)》
《127个常用的JS代码片段,每段代码花30秒就能看懂-【上】》
《深入浅出讲解JS中this/apply/call/bind巧妙用法【实践】》
《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》
《面试中教你绕过关于 JavaScript 作用域的 5 个坑》
作者:吾名云中桥
转发链接:https://mp.weixin.qq.com/s/-UHOsR26jnnogHqPfPM0rQ
猜你喜欢
- 2025-06-24 服务端性能测试实战3-性能测试脚本开发
- 2025-06-24 跨标签页通信(六):Cookie(cookie跨页面)
- 2025-06-24 崩溃!代码总掉链子?6 个 JavaScript 技巧助你稳操胜券
- 2025-06-24 如何取消一个已经开始的 JavaScript Promise
- 2025-06-24 Web页面如此耗电!到了某种程度,会是大损失
- 2025-06-24 setTimeout 出大 bug?为什么需要 setBigTimeout?
- 2025-06-24 悠然!午休十分钟分清 Vue 的 watch 和 computed,面试不发怵
- 2025-06-24 科普:CPU空闲时在忙什么?(cpu空闲温度)
- 2025-06-24 html5大神结合js带你研究古老读心术,你的心思早被猜透
- 2025-06-24 JWT + Refresh + SSO模版(jwt demo)
你 发表评论:
欢迎- 628℃几个Oracle空值处理函数 oracle处理null值的函数
- 621℃Oracle分析函数之Lag和Lead()使用
- 610℃0497-如何将Kerberos的CDH6.1从Oracle JDK 1.8迁移至OpenJDK 1.8
- 604℃Oracle数据库的单、多行函数 oracle执行多个sql语句
- 602℃Oracle 12c PDB迁移(一) oracle迁移到oceanbase
- 594℃【数据统计分析】详解Oracle分组函数之CUBE
- 584℃最佳实践 | 提效 47 倍,制造业生产 Oracle 迁移替换
- 568℃Oracle有哪些常见的函数? oracle中常用的函数
- 最近发表
-
- oracle 19cOCM认证有哪些内容(oracle认证ocm月薪)
- Oracle新出AI课程认证,转型要持续学习
- oracle 表的查询join顺序,可能会影响查询效率
- Oracle DatabaseAmazon Web Services正式可用,Oracle数据库上云更容易了
- Oracle 19.28 RU 升级最佳实践指南
- 汉得信息:发布EBS系统安装启用JWS的高效解决方案
- 如何主导设计一个亿级高并发系统架构-数据存储架构(三)
- Java 后端开发必看!工厂设计模式轻松拿捏
- ORA-00600 「25027」 「x」报错(抱错孩子电视剧 爸爸是武术 另一个爸爸是画家)
- 新项目终于用上了jdk24(jdk新建项目)
- 标签列表
-
- 前端设计模式 (75)
- 前端性能优化 (51)
- 前端模板 (66)
- 前端跨域 (52)
- 前端缓存 (63)
- 前端aes加密 (58)
- 前端脚手架 (56)
- 前端md5加密 (54)
- 前端路由 (61)
- 前端数组 (73)
- 前端js面试题 (50)
- 前端定时器 (59)
- 前端获取当前时间 (50)
- Oracle RAC (76)
- oracle恢复 (77)
- oracle 删除表 (52)
- oracle 用户名 (80)
- oracle 工具 (55)
- oracle 内存 (55)
- oracle 导出表 (62)
- oracle约束 (54)
- oracle 中文 (51)
- oracle链接 (54)
- oracle的函数 (58)
- 前端调试 (52)
本文暂时没有评论,来添加一个吧(●'◡'●)