字节二面:如果高性能渲染十万条数据?

前言

最近博主在字节面试中遇到这样一个面试题,这个问题也是前端面试的高频问题,作为一名前端开发工程师,我们虽然可能很少会遇到后端返回十万条数据的情况,但是了解掌握如何处理这种情况,能让你对前端性能优化有更深的理解,博主在这给大家细细道来。


🚀 作者简介:程序员小豪,全栈工程师,热爱编程,曾就职于蔚来、腾讯,现就职于某互联网大厂,技术栈:Vue、React、Python、Java
🎈 本文收录于小豪的前端系列专栏,后续还会更新前端入门以及前端面试的一些相关文章,手把手带你从零学习前端到面试找工作,并如果有想进入前端领域工作的同学,这个前端专栏会对你有所帮助,欢迎关注起来呀
🌼 本人也会持续的去关注AIGC以及人工智能领域的一些动向并总结到博客中,大家感兴趣的可以关注一下我的人工智能专栏
🌊 云原生的入门学习系列,大家有兴趣的可以看一看

目录

  • 最粗暴的做法(一次性渲染)
  • 使用定时器
    • 为什么会出现闪屏现象呢
    • 简单聊一下 setTimeout 和闪屏现象
  • 使用 requestAnimationFrame
  • 使用 DocumentFragment
  • 总结

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

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

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

总结

各位看官老爷们好,小豪已经建立了技术交流群,如果你很感兴趣,可以私信我加入我的社群。

📝社群中不定时会有很多活动,例如学习资料分享、大厂面经分享、技术讨论、行业大佬创业杂谈等等。

📝本人目前是在互联网大厂正式工作,也有过多个大厂的工作经历,加入社群也会有简历修改辅导,模拟面试,手把手项目实战教学,大厂工作内推机会以及大厂面试题解析分享等福利。

📝社群方向很多,相关领域有Web全栈(前后端)、人工智能、AIGC、自媒体变现、前沿科技文章分享、论文精读等等。

📝不管你是多新手的小白,都欢迎你加入社群中讨论、聊天、分享,加速助力你成为下一个技术大佬!也随时欢迎您跟我沟通,一起交流,一起成长。变现、进步、技术、资料、项目、你想要的这里都会有

📝网络的风口只会越来越大,风浪越大,鱼越贵!欢迎您加入社群~一个人可以或许可以走的很快,但一群人将走的更远!

📝想都是问题,做都是答案!行动起来吧!欢迎评论区or后台与我沟通交流,也欢迎您扫描下方二维码直接加入到我的交流社群!(微信:adcoderhao)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/67699.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Logback日志记录只在控制台输出sql,未写入日志文件【解决】

原因&#xff1a;持久层框架对于Log接口实现方式不一样&#xff0c;日记记录的位置及展示方式也也不一样 mybatis-plus:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # sql只会打印到控制台不会输出到日志文件种mybatis-plus:configuration:log-impl…

31 WEB漏洞-文件操作之文件包含漏洞全解

目录 文件包含漏洞原理检测类型利用修复 本地包含-无限制&#xff0c;有限制远程包含-无限制&#xff0c;有限制各种协议流玩法文章介绍读取文件源码用法执行php代码用法写入一句话木马用法每个脚本支持的协议玩法 演示案例某CMS程序文件包含利用-黑盒CTF-南邮大&#xff0c;i春…

定时任务job (现成直接用)

1. 任务调度的2种方式 1. bean调用示例: 对应的类上加Component或Service注解, 类名.方法名(ryTask.ryNoParams) 类名首字母小写; 该类必须要在com.ruoyi 包下, 因为有校验 2. 权限类名调用示例: com.ruoyi.quartz.task.RyTask.ryNoParams 注意: class(类名)调用的时候, 只能调…

leetcode 35. 搜索插入位置

2023.9.4 本题较为简单&#xff0c;题目要求O(log n)的时间复杂度&#xff0c;就不能遍历查找了&#xff0c;得用二分查找。若成功查找到target值&#xff0c;则直接返回索引&#xff1b; 若直到结束都没查找到则返回left值。(纸上模拟下就懂了) 代码如下&#xff1a; class S…

一个通用的EXCEL生成下载方法

Excel是一个Java开发中必须会用到的东西&#xff0c;之前博主也发过一篇关于使用Excel的文章&#xff0c;但是最近工作中&#xff0c;发现了一个更好的使用方法&#xff0c;所以&#xff0c;就对之前的博客进行总结&#xff0c;然后就有了这篇新的&#xff0c;万能通用的方法说…

flink k8s sink到kafka报错 Failed to get metadata for topics

可能出现的3种报错 -- 报错1 Failed to get metadata for topics [...]. org.apache.kafka.common.errors.TimeoutException: Call-- 报错2 Caused by: org.apache.kafka.common.errors.TimeoutException: Timed out waiting to send the call. Call: fetchMetadata Heartbe…

Axure RP PC电商平台Web端交互原型模板

Axure RP PC电商平台Web端交互原型模板。原型图内容齐全&#xff0c;包含了用户中心、会员中心、优惠券、积分、互动社区、运营推广、内容推荐、商品展示、订单流程、订单管理、售后及服务等完整的电商体系功能架构和业务流程。 在设计尺寸方面&#xff0c;本套模板按照主流的…

logging.level的含义及设置 【java 日志 (logback、log4j)】

日志级别 trace<debug<info<warn<error<fatal 常用的有&#xff1a;debug&#xff0c;info&#xff0c;warn&#xff0c;error 通常我们想设置日志级别&#xff0c;会用到 logging.level.rootinfo logging.level设置日志级别&#xff0c;后面跟生效的区域。r…

酷派30/锋尚40/大观40S首发解锁BL+完美root权限+去除密码黑砖线刷修复

早前的中华酷联&#xff0c;随着时代的发展&#xff0c;酷派手机虽热发展的并没有其他手机那么快&#xff0c;但也 是坚强的活了下来。目前主打机型为Cool系列&#xff0c;最高为Cool30机型&#xff0c;并且发布酷派锋尚 40酷派大观40S&#xff0c;起头并进。该系列机型&#x…

Web前端开发概述

Web&#xff08;World Wide Web&#xff0c;全球广域网&#xff09;是指一种基于互联网的信息系统&#xff0c;通过超文本链接将全球各地的文档、图像、视频等资源相互关联起来&#xff0c;并通过Web浏览器进行交互浏览和访问。Web的发展使得人们可以方便地获取和共享各种类型的…

【Unity】预制体材质变(Clone)克隆体问题

1、排查代码是否存在直接修改预制体的材质为克隆体。 解决&#xff1a;删了这段代码。 2、双击Prefab文件进入预制体编辑模式时&#xff0c;会执行预制体身上的脚本方法Awake、Start等&#xff08;生命周期方法&#xff09;&#xff0c;所以要排查这些方法里是否有克隆…

支持向量机(二)

文章目录 前言具体内容 前言 总算要对稍微有点难度的地方动手了&#xff0c;前面介绍的线性可分或者线性不可分的情况&#xff0c;都是使用平面作为分割面的&#xff0c;现在我们采用另一种分割面的设计方法&#xff0c;也就是核方法。 核方法涉及的分割面不再是 w x b 0 wx…

【搭建私人图床】使用LightPicture开源搭建图片管理系统并远程访问

文章目录 1.前言2. Lightpicture网站搭建2.1. Lightpicture下载和安装2.2. Lightpicture网页测试2.3.cpolar的安装和注册 3.本地网页发布3.1.Cpolar云端设置3.2.Cpolar本地设置 4.公网访问测试5.结语 1.前言 现在的手机越来越先进&#xff0c;功能也越来越多&#xff0c;而手机…

BeautifulSoup模块基本使用方法(解析—提取数据)

一、了解BeautifulSoup 1、简介 一个灵活又方便的网页解析库&#xff0c;最主要的功能是从网页抓取数据&#xff0c;处理高效&#xff0c;支持多种解析器&#xff0c; 它通过转换器实现文档导航、查找、修改文档的方式。利用它可不用编写正则也能方便的实现网页信息的抓取&am…

【Node.js】Express-Generator:快速生成Express应用程序的利器

在Node.js世界中&#xff0c;Express是一个广泛使用的、强大的Web应用程序框架。它为开发者提供了一系列的工具和选项&#xff0c;使得创建高效且可扩展的Web应用程序变得轻而易举。然而&#xff0c;对于初学者来说&#xff0c;配置和初始化Express应用程序可能会有些困难。为了…

【汇编中的寄存器分类与不同寄存器的用途】

汇编中的寄存器分类与不同寄存器的用途 寄存器分类 在计算机体系结构中&#xff0c;8086CPU&#xff0c;寄存器可以分为以下几类&#xff1a; 1. 通用寄存器&#xff1a; 通用寄存器是用于存储数据和执行算术运算的寄存器。在 x86 架构中&#xff0c;这些通用寄存器通常包括…

Redis——数据结构介绍

Redis是一个key-value的数据库&#xff0c;key一般是String类型&#xff0c;不过value的类型是多样的&#xff1a; String&#xff1a;hello wordHash&#xff1a;{name:"Jack",age:21}List&#xff1a;[A -> B -> C -> D]Set&#xff1a;{A,B,C}SortedSet…

uni-app 之 图片

uni-app 之 图片 获取图片 v-bind 动态绑定 image.png <template><view><view>--- 获取图片1 ---<image src"../../static/img/tabbar_home1.png"></image></view><view>--- 获取图片2 v-bind 动态绑定---<image v-bi…

面试题查漏补缺 i++和 ++ i哪个效率更高

i 和 i 哪个效率更高&#xff1f; 在这里声明&#xff0c;简单地比较前缀自增运算符和后缀自增运算符的效率是片面的&#xff0c;因为存在很多因素影响这个问题的答案。首先考虑内建数据类型的情况:如果自增运算表达式的结果没有被使用&#xff0c;而是仅仅简单地用于增加一员…

从本地到Gitee:一步步学习文件上传及解决常见报错问题

&#x1f642;博主&#xff1a;小猫娃来啦 &#x1f642;文章核心&#xff1a;一步步学习文件上传及解决常见报错问题 文章目录 安装git进入gitee官网&#xff0c;登录账号新建仓库先打开git命令行上传本地资源到仓库第一步&#xff1a;git init第二步&#xff1a;git add .第三…