当前位置: 首页 > Web前端 > JavaScript

前端多数据渲染优化

时间:2023-03-26 22:54:35 JavaScript

前言前段时间在做请求的时候,遇到了自定义列表功能。它所有的数据显示都是通过jSON字符串存储的,最开始也是通过JSON解析的。数据是有上限的,但是后来提高上限后,出现了卡顿等问题,所以本文介绍一些解决前端大量数据渲染问题的方案。innerHTML的解决方案是很久以前第一次插入innerHTML渲染解决方案。官方API,性能更好。这是一个简单的HTML渲染例子(实验中数据在10w级别,差异放大,实际中基本会小于这个级别)constitems=newArray(100000).fill(0).map((it,index)=>{return`

item${index}
`}).join('')content.innerHTML=items来自谷歌的性能分析:页面在10秒滚动,可以看到dom的渲染阻塞了页面1300毫秒。性能测试中总阻塞时间控制在300ms以内为合格状态。这个时间也会受到电脑硬件的影响。总结一下这种方式的优缺点:优点:性能还算可以,但是数据量大的时候也有阻塞。缺点:存在注入危险,与框架匹配性差。当dom过多时,滚动的性能问题并没有解决。批量插入是通过分片插入的,如果有10W条数据,我们将其分成10次,每次1w次循环插入[...newArray(10)].forEach((_,i)=>{requestAnimationFrame(()=>{[...newArray(10000)].forEach((_,index)=>{constitem=document.createElement("div")item.textContent=`item${i}${index}`content.append(item)})})})经过谷歌分析:这里也是页面刷新和滚动的性能分析,可以看出阻塞时间为1800毫秒,比innerHTML差一点。这是10w的数量级。数字越小,时间间隔越小。关于requestAnimationFrame中requestAnimationFrame的作用:这个方法会告诉浏览器我要执行动画,请求浏览器在下次重绘前调用回调函数来更新动画执行方法:执行requestAnimationFrame(callback)时,回调回调函数不会立即被调用,而是会被放入回调函数队列,当页面可见且动画帧请求回调函数列表不为空时,浏览器会周期性的将这些回调函数加入到浏览器UI的队列中线程(回调函数的时机由系统决定)。一般来说,它不会阻塞其他代码的执行,但总的执行时间与innerHTML方案相差不大。总结优缺点:优点:不会阻塞代码的运行。长期以来,滚动的性能问题一直没有得到解决。其它原生方法canvascanvas是专门用于绘图的工具,可用于动画、游戏画面、数据可视化、图片编辑和实时视频处理。最近在大名鼎鼎的Flutter框架的web中使用canvas来渲染页面。同样,我们也可以使用canvas来渲染大量数据/div>letctx=canvas.getContext('2d');[...newArray(100000)].map((it,index)=>{ctx.fillText(`item${index}`,0,index*30)})经过实际尝试,canvas是受限的,最大6w左右的高度已经不能放大了,也就是说在大量数据下,canvas还是受限的,有待进一步优化这里是一个优化思路,监听外层DOM的滚动,根据高度动态渲染画布的显示,可以达到最终的效果,但是成本还是太高了。优点:在渲染次数上性能好缺点:如果想实现和虚拟列表一样的渲染,不可控(在其他场景下是更好的解决方案,比如动画,地图等)是难以控制画布中的样式IntersectionObserverIntersectionObserver提供了一个异步观察目标元素和视口的交集状态,简单的说就是我们可以监测一个元素是否会被我们看到。当我们看到这个元素时,我们可以执行一些回调函数来处理某些事务。注意:IntersectionObserver的实现要使用requestIdleCallback(),即观察者只有在线程空闲的时候才会执行。这意味着这个watcher的优先级很低,只有在其他任务完成且浏览器空闲时才会执行。通过这个api,我们可以做一些尝试来实现一个类似虚拟列表的解决方案。这里我实现了一个向下滑动的虚拟列表demo。主要思想是监控列表中的所有dom。当它消失时,移除并移除monitor,然后添加新的DOM和监听器核心代码:constintersectionObserver=newIntersectionObserver(function(entries){entries.forEach(item=>{//0表示消失if(item.intersectionRatio===0){//最后添加intersectionObserver.unobserve(item.target)item.target.remove()addDom()}})});Google的性能分析(第一次进入页面,连续滚动1000条):可以看到基本没有阻塞,这个方案是可行的,初始渲染和滚动之间没有问题。详情请点击查看。demo只实现了滚动的解决方案:https://codesandbox.io/s/snow...进一步优化,现在实现了IntersectionObserver具有类似虚拟列表的功能,不过貌似会有隐患经常加监听和取消,所以打算采用扩展方案:总体思路:当前列表10个为一组,当前列表一共渲染30个,当滚动到第20个时,触发一个事件,加载第30个-40,同时删除0-10,依次触发。这样,触发次数和监听次数都会呈指数级下降。当然,成本是同事渲染的dom数量增加,以后我们会增加每个团队的数量,从而在dom数量和监控之间保持一个更平衡的状态。兼容性关于IntersectionObserver的兼容性,通过polyfill,可以得到大部分浏览器的兼容性,最低支持IE7。具体可以参考:https://github.com/w3c/Inters...总结的很好缺点:优点:使用原生API实现的虚拟列表方案,无数据瓶颈缺点:框架在生产中的适应性不够高,实现比较复杂无限滚动中频繁触发监听和释放,可能会出现一些问题withtheframework上面提到的很多方法都是在非framework中实现的,这里我们看一下列表在react中的表现react这是一个长度为10000的列表渲染函数App(){const[list,setList]=使用状态([]);useEffect(()=>{setList([...newArray(50000)]);},[]);return({list.map((item,index)=>{returnitem{index}
;})}
);}运行demo时,可以明显感觉到页面卡住了。谷歌分析,在50000的数量级,重新刷新后,10秒内仍然没有渲染完成。当然,在框架中的表现肯定没有原来那么强。这个结论是意料之中的。在线演示地址:https://codesandbox。io/s/angr...还有一点需要注意的是,模板中大量数据的传输//这个列表的数量级是几千甚至几万,会导致卡顿增加呈指数增长。这个结论对vue和react都适用,所以大量数据的传递必须在内存中赋值和获取,而不是通过modules、renders等常规方式。如果数量级是100,我们还可以考虑优化,可以加起来更多startTransition在react18中,会有一个新的APIstartTransition:startTransition(()=>{setList([...newArray(10000)]);})这个API的功能类似于我上面提到的requestAnimationFrame,他它不会增强性能,但可以避免延迟,先渲染其他组件,避免白屏虚拟列表。这里正式引入虚拟列表的概念,用于存储所有列表元素的位置,只在视口中渲染列表元素。当可视区域滚动时,根据滚动偏移大小和所有列表元素的位置计算可视区域应该渲染哪些元素一张动图看懂原理:最小化实现方案这里我们尝试自己实现一个最小化虚拟列表方案//这是一个reactdemo。在vue项目中,原理类似,只是数据源的设置不同。基本没有变化//数据源和配置属性consttotalData=[...newArray(10000)].map((item,index)=>({index}))consttotal=totalData.lengthconstitemSize=30constmaxHeight=300functionApp(){const[list,setList]=useState(()=>totalData.slice(0,20));constonScroll=(ev)=>{constscrollTop=ev.目标。scrollTop常量startIndex=Math.max(Math.floor(scrollTop/itemSize)-5,0);constendIndex=Math.min(startIndex+(maxHeight/itemSize)+5,总计);setList(totalData.slice(startIndex,endIndex))}return({list.map((item)=>{returnitem{item.index}
;})}
);}可以查看在线demo:https://codesandbox.io/s/agit...这是一个虚拟列表的最小例子,主要分为2部分。容器被包裹起来,使用CSS来增加高度。实际渲染出来的item需要使用transform来显示到正确的位置来监听外部容器的滚动。滚动时动态切片原始数据源,替换要显示的列表查看性能:基本无阻塞,偶尔会有一点丢帧。这个演示不是最终形式。还有很多地方需要优化比如缓存,逻辑抽取,CSS再次简化,控制滚动的触发频率,滚动方向控制等等,还有很多可以优化的点其他库react-virtualized很多库推荐virtuallist解决方案,大而全的react-windowreact-virtualized推荐库,更轻量级的替代方案react-virtualvirtuallistHooks形式,类似于我demo中的逻辑封装Chrome正式支持virtual-scroller在Chromedevsummit2018上,谷歌工程经理GrayNorton介绍virtual-scroller对我们来说,一个Web滚动组件,未来可能成为高级Web分层API(LayeredAPI)的一部分。它的目标是解决长列表的性能问题,消除离屏渲染。不过开发部分,经过内部讨论,还是先终止这个API,转向CSS开发链接:https://github.com/WICG/virtu...Chrome对virtual-scroller的介绍:https://chromestatus.com/feat...content-visibility这是后来开发的新CSS属性。Chromium85开始有了content-visibility属性,这可能是提高页面加载性能最有效的CSS属性。content-visibility使用户代理正常跳过元素渲染工作(包括布局和绘画),除非需要。如果页面有大量离屏内容,可以通过content-visibility属性跳过对离屏内容的渲染,加快用户首屏的渲染时间,减少用户交互等待时间页。具体介绍:https://web.dev/content-visib...使用方式是直接添加CSS属性#content{content-visibility:auto;}可惜它的作用是增强渲染性能,但是在初始化大量数据的时候还是会卡顿,不如虚拟列表直接有效,但是当我们减少首屏渲染时间的时候,可以考虑用它来总结性能多数据下的优化,有很多解决方案