当前位置: 首页 > news >正文

前端性能优化-虚拟滚轮(Virtual Scroll)

更多推荐阅读:

Nginx实战-CSDN博客

关于列表性能分析与标准-CSDN博客

Fullcalendar常用功能介绍-CSDN博客


目录

浏览器渲染原理

性能瓶颈

数据分页

无限滚动

监听滚动事件更新列表内容(handleScroll方法)

1. 计算可视的列表范围

2. 计算上下填充高度

页模式


提到前端性能优化问题,相信大家首先会想到的现象就是页面卡顿。当然,页面很“卡”这个表现,用前端的视角来看,也有几种不同的诱因。例如 http 请求过多导致数据加载很慢、下载的静态文件非常大导致页面加载时长很高(CSS,JS,静态图片等)、js在密集型计算中本身性能拉胯等等。当然,这里主要的就是通过DOM渲染来切入。

浏览器渲染原理

一个完成的网页也是HTML、CSS、JS三个元素组合而成的,这里可以拿乐高对比,HTML好比是不同长宽、大小的积木块,CSS 就是这些积木块的颜色,JS便是积木块的一些交互事件,例如有些积木块是轮胎可以滚动并带动整个上层的积木块产生位移等。而浏览器渲染自然也会去解析这三块内容并拼凑成一个完整的网页:

  • HTML/SVG/XHTML 解析提供的html文件会产生一个 DOM Tree。
  • CSS 解析CSS会生成一个CSS Rule Tree
  • 在渲染阶段,浏览器会把 DOM Tree 和 CSS Rule Tree 进行一个结合,生成一个最终的 DOM Tree并赋予每个树节点样式,生成 Render Tree
  • 布局 Render Tree(layout/reflow),绘制各元素尺寸、位置计算
  • 绘制 Render Tree (paint),绘制页面像素信息
  • 将计算好的信息发送给 GPU 并显示在屏幕上

举个例子,下方HTML文本在浏览器中的渲染过程:

<html>   <header>qi qiao</header>   <body>     <div>       <h1>Title 1</h1>       <p>Content</p>     </div>   </body> 
</html>

但是,网页不是纯静态的,我们势必会在页面上进行交互,而交互过后的结果也会导致页面上部分元素产生变动,例如在样式或布局上,这就成为回流(重排)和重绘。

  • 重绘:当 Render tree 中的一些元素需要更新属性,而这些属性只影响到元素的外观(例如颜色)而不会影响到整体布局
  • 回流(重排):当 Render Tree 中的一部分(或全部)元素因为规模、尺寸等等其他因素而改变时则需要重新构建 DOM Tree

性能瓶颈

回流必定发生重绘,而重绘不一定会引发回流。由此可以推断,回流所带来的成本是非常高的,如果频繁触发回流操作,那势必会造成页面的卡顿。已知下列操作都会导致回流和重绘:

  • 页面第一次渲染(初始化)
  • 添加或删除可见的 DOM 元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化(包括外边距,内边距,边框大小,高度和宽度等)
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代或者字体改变
  • 浏览器窗口尺寸的变化(因为回流是根据视觉窗口的大小来计算元素的位置和大小的)
  • 定位或者浮动,盒子模型(相关属性:weidth/height)等

当然,由于每次重排都会造成巨大的性能消耗,大多数浏览器本身会实现一个队列去记录每次重排的动作,当队列中的动作达到一定的阈值或是经过一定的时间后会批量清空队列并进行一次重排,这样就会让原本多次的重排压缩成一次处理。

但是凡事没有绝对,当出现获取布局信息的操作时(这里可以理解为用户的某些行为需要看到浏览器页面的直接反馈效果),会立即刷新该队列的刷新并触发重排:

  • offsetTop, offsetLeft, offsetWidth, offsetHeight(获取元素位置信息)
  • scrollTop, scrollLeft, scrollWidth, scrollHeight(溢出类元素的滚动条操作)
  • clientTop, clientLeft, clientWidth, clientHeight(获取滚动条偏移量)
  • getComputedStyle()【getComputedStyle(样式元素,null)[样式名] 的形式来获取某个元素的当前样式】
  • getBoundingClientRect()【getBoundingClientRect() 返回的是矩形的集合。表示了当前盒子在浏览器中的位置以及自身占据的空间的大小,除了 width 和 height 之外,其他的属性是相对于视图窗口的左上角来计算的。】

这里是用 scroll event(滚动事件)的发生来讲解性能优化。我们需要明确,不是滚动事件本身就一定意味着性能上的消耗,而是该动作如果伴随着大量的元素参与到了重排的动作中,那么才会引起性能上的焦虑。这里需要注意,我们加粗了“大量”这个关键词,俗话说得好,量变引起质变,即使是频繁重排但是只涉及到两三个元素,以现在我们的硬件性能也不在话下。但问题就在于特殊的业务场景,例如需要在页面上表现一个超长的表格(可能上万行数据),就是需要将所有数据一次性展示在页面中,并且在滚动时及时显示对应偏移量的数据,那么每一次滚动都是对这几万个元素的重排,性能自然不堪入目。

我们可以得出浏览器的性能瓶颈在于两点:

  • 无法一次性渲染太多的 DOM 元素
  • 每一次滚动事件将会让对应 DOM 中的所有元素重新渲染

目前常用的解决手段是数据分页无限滚动

数据分页

这个方案是大家浏览到页面所常用的,通常在需要展示非常多行的数据时,页面会采用分页的做法来分割数据,虽然该方法减少了一次性所渲染的行数,但是如果查询的表列数非常多的话,还是有很大概率需要渲染非常多的元素,所以不是一个稳妥的选型。

无限滚动

该方案的解决方法是第一次只渲染所能承受范围内的数据量,当滚动条拖动接近底部(或右部)时,再去追加下一批所需要渲染的元素,该方案也是有一个明显的缺陷在于,无限地滚动下去必然会触及浏览器的性能瓶颈,而且所需要渲染的元素会越来越多,性能迟早会被拖垮。

虚拟滚动:简单的说就是渲染在浏览器中当前可见范围内的内容,通过用户滑动滚动条的位置动态地计算显示内容,其余部分用空白填充来给用户造成一个长列表的假象;

数据分页的方案是一次性渲染固定行数和列数的数据量,缺点是怕一次性的量就逼近上限。无限滚动的方案是想看更多数据的时候再继续渲染,不看就不渲染避免性能浪费,但缺点就在于只要一直触发“继续看”的操作,那么之前遗留的数据将会越来越多导致性能雪崩。

这时候我们通过计算可视范围内的单元格,这样就保证了每一次拖动,我们渲染的 DOM 元素始终是可控的,不会像数据分页方案怕一次性渲染过多,也不会发生无限滚动方案中的老数据堆积的现象。接下来我们用一张图来表示虚拟滚动的表现形式。

根据图中我们可以看到,无论如何滚动,我们可视区域的大小其实是不变的,那么要做到性能最大化就需要尽量少地渲染 DOM 元素,而这个最小值就是可视范围内需要展示的内容,也就是图中的绿色区块,在可视区域之外的元素均可以不做渲染。

那么问题来了,如何计算可视区域内需要渲染的元素,我们通过如下几步来实现虚拟滚动:

  • 每一行的高度需要相同,方便计算
  • 需要得知渲染的数据量(数组长度),可基于总量和每个元素的高度计算出容器整体的所需高度,这样就可以伪造一个真实的滚动条
  • 获取可视区域的高度
  • 在滚动事件触发后,滚动条的距顶距离也可以理解为这个数据量中的偏移量,再根据可视区域本身的高度,算出本次偏移的截止量,这样就得到了需要渲染的具体数据
  • 如果类似于渲染一个宽表,单行可横向拆分为多列,那么在X轴上同理实现一次,就可以横向的虚拟滚动

例子:

<div style="overflow-y: scroll; height: 300px;" @scroll="handleScroll">   <div v-for="item in items" :key="`${item.id}`">        <slot :data="item">        </slot>    </div></div>

有了这个可自定义dom的列表结构后,在外面套一层可滚动的定高的容器,我们就实现了一个所有列表类组件的基础dom。那么接下来要做的就是填充可视列表以外的滚动高度。这个做法有挺多的,比如在列表上下定义,通过改动高度来控制总高度;比如通过控制列表的padding-top与padding-bottom来控制;再比如直接将列表高度设置成所有元素高度总和,通过定义position启用top来进行定位也是可以的......有了这个dom结构后我们就可以对显示内容进行计算了。

监听滚动事件更新列表内容(handleScroll方法)

1. 计算可视的列表范围

首先我们可以确定的是列表的可视范围是一段连续的数组内容,于是这个计算就被简化为了找到连续数组内容的开始点与结束点。开始点与结束点依赖的两个信息:一是列表每一项的具体y坐标,二就是当前可视范围的开始点与结束点(下图的s与e)。列表每一项的y坐标可以用一次循环通过累加每项的高度来得到每项的y坐标;

当数组每一项为固定高度的时候,我们可以直接用s除以每项高度向下取整(floor)来得到上界,用e除以每项高度向上取整(ceil)来得到下界,然后用slice方法获得最终需要展示的元素数组。

当数组每一项都为非固定高度的时候,我们采用二分法来寻找数组的上界与下界,当然这种情况用虚拟滚轮也很复杂。

2. 计算上下填充高度

有了数组的上界与下界,上下填充高度的计算也就可以设置列表的padding-top与padding-bottom属性。

页模式

很多时候我们需要优化的不是一个长列表,而是一个长页面,那么对于上述的计算方法有什么改变呢?

首先我们需要改变可视范围开始点s与结束点e的计算方法,对于页面而言可视范围的开始点即是window.pageYOffset || document.documentElement.scrollTop;结束点是开始点加上可视范围的高度,这里的高度计算我们使用window.innerHeight || document.documentElement.clientHeight,但请注意这两个属性在页面有滚动条的时候返回的值是不同的,innerHeight会包含滚动条的高度,clientHeight不包含滚动条的高度。

计算完了可视范围,我们还需要调整数组y坐标。原先的数组y坐标都是相对于滚动容器而言的,现在我们需要将数组的y坐标调整为相对于页面。调整方法有两种:一是可以在计算y坐标的时,加上滚动容器的offsetTop属性;二是可以在计算可视范围开始点s与结束点e时,减去滚动容器的offsetTop属性。

调整完了坐标,我们还需要将滚动容器的height与overflow-y属性去掉,让容器自由生长,同时将滚动容器的scroll事件转移到window对象上,这样就实现了对页的虚拟滚动。通过页模式,我们就可以实现对任何通过固定高度块布局的长页面进行此类的优化。


作者:道一云低代码

作者想说:喜欢本文请点点关注~

更多资料分享

相关文章:

  • 无菌药厂通信架构升级:MODBUS TCP转CANopen技术的精准控制应用
  • VUE3 ref 和 useTemplateRef
  • JAVA 线程池 BlockingQueue详解
  • LMKD(Low Memory Killer Daemon)原理初识
  • wandb转为csv
  • LeetCode - 238. 除自身以外数组的乘积
  • Vue 模板配置项深度解析
  • ArcPy扩展模块的使用(3)
  • Quick BI 自定义组件开发 -- 第二篇 添加 echart 组件,开发图表
  • Redis群集
  • 精准夹持,稳定控制:IXTUR气控永磁铁选型全攻略(涵盖MAP、MRP与LI-120系列)
  • push [特殊字符] present
  • 【数据集处理】拼接MODIS 1 kmNDVI数据集(MRT工具处理+Python全代码)
  • 【大厂机试题解法笔记】报文响应时间
  • Qt+OPC开发笔记(二):OPC客户端介绍与读取和写入bool类型Demo
  • rknn toolkit2搭建和推理
  • rknn优化教程(二)
  • 零基础设计模式——行为型模式 - 责任链模式
  • Jenkins自动发布C# EXE执行程序
  • UI框架-通知组件
  • 关于网站建设心得体会/必应搜索国际版
  • 沈阳网站建设渠道/抖音引流推广怎么做
  • 公司网站成本/网络营销策划
  • 自己怎么去做seo网站推广?/上海aso
  • 网上做问卷调查网站/微信管理系统登录入口
  • wordpress日记网站/创建网站步骤