回答:
在大型的企业级项目中经常要渲染大量的数据,这种长列表是一个很普遍的场景,当列表内容越来越多就会导致页面滑动卡顿、白屏、数据渲染较慢的问题;
这种情况主要发生在小程序、移动端或者后台管理的页面当中;
通常我们会采用分页的方式进行内容的逐渐获取,但是当内容越来越多时;
比如移动端的下拉刷新,不停的往上翻,到底部会加载更多内容,这样一来列表中会新增很多的元素,元素多了以后会触发浏览器的重排重绘,无论是内存的占用还是GPU的渲染都会带来很大的性能损耗,导致页面滑动卡顿、数据渲染较慢的问题;
处理时要根据情况做不同处理:
- 避免大数据量:采取分页的方式获取
- 避免渲染大量数据:vue-virtual-scroller 插件等虚拟列表方案,只渲染视口范围内数据
- 避免更新:使用 v-once 方式只渲染一次
- 优化更新:通过 v-memo 缓存子树,有条件更新,提高复用,避免不必要更新
- 按需加载数据:采用懒加载的方式,比如 tree 组件子树的懒加载
总之,还是要看具体需求,首先从设计上避免大数据获取和渲染;实在需要这样做可以采用虚拟列表的方式优化渲染数量;最后优化更新,如果不需要更新可以 v-once 避免更新,需要更新可以 v-memo 进一步优化大数据更新性能,其他可以采用的是交互方式优化,无限滚动,懒加载方案等。
页面卡顿的原因
根源:大量DOM元素的 reflow 和 repaint
修改是对当前DOM元素的修改,更新却是所有的DOM都更新
优化思路:
1、懒渲染
- 懒加载,常见的长列表优化方案,常见于移动端
- 原理:每次只渲染一部分,等渲染的数据即将滚动完时,再渲染下面部分
- 优点:每次渲染一部分数据,速度快
- 缺点:数据量大时,页面中依然存在大量DOM节点,占用内存过多,降低浏览器渲染性能,导致页面卡顿
- 使用场景:数据量不大的情况下(比如1000条,具体还要看每条数据的复杂程度)
2、分页渲染
一般是后端给我们数据,我们只需把页码数,每页展示的数据量,给后端,后端给我们数据我们进行展示即可。
3、可视区域渲染
原理: 只渲染页面可视区域的列表项,非可视区域的数据 “完全不渲染”(预加载前面几项和后面几项) ,在滚动列表时动态更新列表项,为了防止白屏,所以实际会多加载几条数据
自己实现可视区渲染
父组件中
- 使用 computed 缓存模拟一万条数据,
- 通过属性绑定将 item (数据)、 size (每条数据的高度)和 showNumber (每次渲染的数据条数)传递给子组件
子组件中
- 通过 props 接收父组件传递过来的属性;
- 最外层盒子设置 overflow-y: scroll; 实现纵向滚动;
- 最外层盒子固定高度 (最外层盒子高度 = size * showNumber);
- 给外层盒子监听滚动事件,计算被卷起的数据高度;
- 计算可视区数据的起始索引,start = 卷起的高度 / 单条数据高度;
- 计算可视区数据的结束索引,end = 起始索引 + 可视区可展示的条数;
- 取可视区域起始索引 start 和结束索引 end 的数据展示到可视区域;
父组件
模拟一万条数据
创建了一个拥有10000个元素的空数组,利用 fill() 函数初始化,所有的值都为空,再利用 map, 给数组返回 id 和 content,也就实现了数组赋值;
computed: {
item () {
return Array(10000).fill('').map((item, index) => ({
id: index,
content: `列表内容`+ index
}))
}
}
子组件
ui结构
list 是可视区域
bar 是撑开盒子,使其能纵向滚动
- 可视区容器:可以看作是在最底层,容纳所有元素的一个盒子。
- 可滚动区域:可以看作是中间层,假设有 10000 条数据,每个列表项的高度是 50,那么可滚动的区域的高度就是 10000 * 50。这一层的元素是不可见的,目的是产生和真实列表一模一样的滚动条。
- 可视区列表:可以看作是在最上层,展示当前处理后的数据,高度和可视区容器相同。可视区列表的位置是动态变化的,为了使其始终出现在可视区域。
理解以上概念之后,我们再看看当滚动条滚动时,我们需要做什么:
- 根据滚动距离和
item
高度,计算出当前需要展示的列表的startIndex
- 根据
startIndex
和 可视区高度,计算出当前需要展示的列表的endIndex
- 根据
startIndex
和endIndex
截取相应的列表数据,赋值给可视区列表,并渲染在页面上 - 根据滚动距离和
item
高度,计算出可视区列表的偏移距离startOffset
,并设置在列表上
接收父组件传递过来的数据 ,定义初始下标和结束下标
计算容器高度和被卷起的数据高度
给container监听滚动事件,起始下标就是卷去的数据条数,向下取整
结束下标就是 起始下标 + 要展示的数据条数
使用虚拟列表 vue-virtual-scroller 实现
对于长列表来说,我们大部分的操作都是:
1.懒加载,分页,
2.Object.freeze冻结数组取消响应式,因为大多时候都是展示
3.高清图替换成缩略图,因为很多时候长列表的图尺寸都比较小,所以可以用小图来代替
以上能解决大部分的长列表问题,在可以分页的情况下
当不能分页时,采用可视区渲染
items高度固定(RecycleScroller)
下载插件
yarn add vue-virtual-scroller --save
在 main.js 注册
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
import VueVirtualScroller from "vue-virtual-scroller";
Vue.use(VueVirtualScroller);
以下是必填项
Props | 解释 | 默认值 |
items | 可视区域要渲染的数据列表 | -- |
item-size | 每项数据的高度 | null |
style | 可视区域的外层盒子高度 | -- |
v-slot = { item } | 插槽,拿到每项数据item | -- |
以下按需要修改
Props | 解释 | 默认值 |
prerender | 告诉服务端(SSR)每次渲染几条数据 | 0 |
buffer | 可视区域外多渲染的数据高度,避免滚动留白 | 200 |
keyField | 用于标识项目和优化管理渲染视图的字段 | id |
固定高度的作用域插槽参数
Slot | 解释 | 默认值 |
item | 每项数据 | -- |
index | 每项数据的下标 | -- |
active | 视图是否处于活动状态。活动视图被视为可见视图,并由RecycleScroller定位。非活动视图不被视为可见,并且对用户隐藏。如果视图处于非活动状态,则应跳过任何与渲染相关的计算。 | -- |
items高度固定(RecycleScroller),可上拉加载
Events | 解释 | 默认值 |
resize | 重新计算大小时触发 | -- |
visible | 当滚动条认为自己在页面中可见时触发 | -- |
hidden | 当滚动条隐藏在页面中时触发 | -- |
update (startIndex, endIndex) | 每次更新视图时发出,仅当 :emitUpdate=“true”时 | false |
实现思路:
- 添加 :emitUpdate="true" 和 @update 事件;
- update函数传入两个形参,start 和 end ,判断当 end 等于数组列表的长度时,去请求接口来获取新的数据,然后加入到数组当中。
items高度不固定(DynamicScroller),可上拉加载
Props(参数) | 解释 | 默认值 |
item(必填) | 每项数据 | -- |
active(必填) | 保持视图,数据处于 active 状态,将防止不必要的大小重新计算。 | -- |
sizeDependencies | 影响高度的值,如果发生变化,则重新计算 | -- |
watchData | 深入监视更改以重新计算大小(不推荐,可能会影响性能) | false |
tag | 组件要呈现的元素 | div |
emitResize | 每次重新计算大小时发出事件(可能会影响性能) | false |
minItemSize | 列表项初次渲染使用的最小高度 |
Events(事件) | 解释 | 默认值 |
resize | 重新计算大小时触发,仅当 :emitUpdate=“true”时 | false |
小程序长列表优化实践 - 知乎