前端性能优化-无限滚动的性能瓶颈

1,427 阅读9分钟

今天来分享一个在实际场景中大家高概率会遇到的关于滚动功能的性能瓶颈问题,这个问题经常会出现在以下两个高发场景

  • 海量数据的手动无限滚动加载问题
  • 活动场景海量数据自动轮播滚动问题

两个场景都必须具备“海量数据”,数据基数要足够大,千条数据以内的场景就蹦麻烦了,问题现象也比较明显,随着时间推移,数据量上涨,dom渲染数的增多,浏览器内存的持续上升,如果再加上数据频繁更新重绘,页面会逐渐变得卡顿,特别是第二个场景的滚动动画也出现了变慢变卡的现象,刚好我这次遇到的也是第二个场景,比较有代表性,也能涵盖第一个场景的问题和解决方案,下面来展开讲讲事情的来龙去脉和最终的解决方案。

一、背景

发生场景:问题出现在大屏展示的业务中,多个监考学校学生数据“实时”显示系统,系统左侧两个区域的数据都是自动滚动显示,附图:

屏蔽信息.pic.jpg 问题现象:随着时间的推移,滚动区域动画滚动速度变慢,甚至低配机型会出现卡住不动的现象。

问题分析

  1. 左上滚动区域每80s加载100条数据,共10+页,最多加载量1000+条。
  2. 左下滚动区域三页数据,每页三列,每列数据量级在700+,不过按照产品需求三页数据无论何时切换滚动状态是不停止的,也就相当于最多6000+条数据量同时滚动,并且我们前面提到了数据可能存在更新的情况,通过定时器获取数据的形式来实现数据更新,每5秒调用一次接口更新一整页数据,为了优化性能原有的处理方案是每5秒更新一页三个列表均新增30条数据,频繁的数据更新以及页面重绘在时间的催化剂下问题依旧会显现。
  3. 为了实现滚动效果,使用了三方自动滚动插件vue-seamless-scroll,翻看源码此插件渲染时使用了双层dom实现,相当于渲染量翻倍,重绘频率也翻倍,并且作者在滚动数据量超过100条时就提示了数据超载,也就是说这个插件本身就没有解决大数据量的数据加载和渲染问题,不适合这个场景进行使用。

vue-seamless-scroll.jpg

总体来说问题发生了两个主要原因就是同屏dom渲染数据过大数据更新和页面重绘频率过高

二、制定方案

了解了业务场景和实现逻辑的基础上,明确了问题产生原因,制定应对方案也就清晰了,下一步针对这种常见场景制定优化方案

  • 频繁的滚动动效首先可以想到开启GPU加速减轻CPU负担,更快地渲染图像和动画。
  • 提高动画滚动速度:这个可能是容易忽略的,针对这种自动滚动的场景滚动速度一定不能设置的太低,过慢的滚动速度意味着页面元素需要较长时间在屏幕上保持可见状态,这可能增加页面的计算负荷。特别是当滚动涉及到大量的数据绑定或实时元素更新时,过慢的速度会导致浏览器需要处理更多的计算任务,从而影响整体的页面性能。浏览器的帧率通常是60fps,这意味着每帧间隔大约16.67毫秒。如果滚动速度过低,可能无法在每帧合理地更新元素的位置,导致动画看起来不连贯或跳跃。
  • 降低数据更新的频率,数据更新过于频繁,是导致页面卡顿的关键所在,需要优化数据更新的dom渲染的业务逻辑。
  • 减少同时滚动dom数,现有插件的实现方案使得dom数翻倍,首先要替换掉插件,自定义或寻找高性能插件实现。

三、方案实施

首先来制定第一套解决方案,目标是把重点问题进行快速优化通过实际场景来观察最终效果: 具体举措:

  1. 开启gpu加速 will-change: transform
  2. 提高动画滚动速度,减少transform动画执行间隔。
  3. 优化数据更新逻辑,只有确认数据更新后才去操作更新列表数组。
  4. 考虑到动画效果比较简单可以自定义一个滚动组件来替换掉原有滚动插件,为了保障动画效果依旧沿用translate动画,dom渲染进行优化,深拷贝传入组件的列表数据,每次从拷贝的数组中取指定数量数据(eg:60)判断滚动到底部时再获取更多数据进行加载,这样既减少了滚动dom的数量也降低了数据更新的频率,并且不破坏原逻辑处理,最小代价完成优化。

经过数据模拟真实使用场景测试,此优化方案已经完全能应对这个需求场景,没有卡顿和过高的内存占用问题,到此可以说是已经解决了这个场景下的历史问题。

但是部分读者这时候可能并不满足,上面的方案针对千级数量级的场景没有问题,但是如果是遇到万级数量级以及更长时间的滚动时长下内存还是会持续上升,页面还是会出现卡顿问题。为了彻底解决这种场景的性能瓶颈,我们继续来看升级解决方案。

四、方案升级

下面我们来思考真正海量数据场景的性能问题应该如何解决,并且针对于上文提出的两种高发场景下的问题能否通过自定义一个公共插件提供统一的解决方案? 我们来寻找一下两个场景的共性问题:

  1. 内存使用过高:随着用户不断滚动,页面上动态加载的元素数量持续增加,这些元素没有被适当地管理和回收,会导致浏览器的内存使用不断增加。这会使得页面变得越来越卡顿,甚至出现浏览器崩溃的情况。

  2. 渲染延迟:如果每次滚动都需要加载和渲染大量的新元素,因为DOM的频繁更新页面重排以及重绘,浏览器的渲染过程可能会变慢,导致用户体验到明显的滚动卡顿。

  3. 滚动性能不佳:大量数据和元素的不断加载可能会影响滚动的流畅性,特别是在计算能力较弱的设备上,滚动过程中可能会出现跳帧或延迟响应。

针对于上面问题我们来制定最终的优化方案:

无限滚动场景:通过虚拟滚动技术动态地加载和卸载DOM元素,只渲染当前视口附近的内容,定期清理不再需要的DOM元素和数据,这可以显著降低内存使用和提高渲染性能。

自动轮播场景:在虚拟滚动的基础上我们通过requestAnimationFrame来动态控制每帧滚动的像素值实现滚动动效。

图示: 图例.jpg

五、插件封装发布

有了理论解决方案下面讲解下代码实现,并把它封装成插件发布到npm提供通用插件使用。

  1. 组件包含一个外部容器 listContainer 和两个内部容器 listPhantom 与 list。listPhantom 用于设置整个滚动内容的高度,而 list 则用于实际显示滚动内容的部分。
<div ref="listRef" :style="{ height: containerHeight }" class="listContainer" @scroll="scrollEvent">
    <div class="listPhantom" :style="{ height: computedListHeight + 'px' }"></div>
    <div class="list" ref="infiniteListRef" :style="{ transform: computedGetTransform }">
      <div ref="items" class="listItem" v-for="item in computedVisibleData" :key="item.id">
        <slot v-bind="item" />
      </div>
    </div>
  </div>

  1. 使用计算属性 computedVisibleData 来确定哪些数据应该在当前视口中显示。这通过以下方式完成:
  • 计算开始索引 (start):基于当前滚动位置 (scrollTop) 和每个元素的高度 (itemHeight) 计算出第一个应该显示的元素的索引。
  • 计算结束索引 (end):通过添加视口能包含的元素数量来确定最后一个应该显示的元素的索引。
computed: {
    computedVisibleData() {
      return this.dataArray.slice(
        this.start,
        Math.min(this.end, this.dataArray.length)
      );
    }
  }

3.滚动事件处理:每次触发滚动事件时,scrollEvent 方法会更新 start 和 end 索引,并重新计算 startOffset,这是当前滚动位置与第一个可见元素顶部的偏移量。这确保了随着用户的滚动,视图可以实时更新以显示新的可见数据。

scrollEvent() {
      let scrollTop = this.$refs.listRef.scrollTop;
      this.start = Math.floor(scrollTop / this.classOption.itemHeight);
      this.end = this.start + this.computedVisibleCount;
      this.startOffset = scrollTop - (scrollTop % this.classOption.itemHeight);
}

4.添加自动滚动:使用requestAnimationFrame来控制每一帧动画的滚动距离scrolltop。

startAutoScroll() {
      this.isScrolling = true;
      const maxScroll = () =>
        this.$refs.listRef.scrollHeight - this.$refs.listRef.clientHeight;
      const animateScroll = () => {
        if (this.$refs.listRef.scrollTop < maxScroll()) {
          this.$refs.listRef.scrollTop += this.classOption.increment;
          this.scrollFrame = requestAnimationFrame(animateScroll))
        } else {
          this.isScrolling = false; // 到达底部停止滚动
        }
      };

      this.scrollFrame = requestAnimationFrame(animateScroll);
}

至此插件主要代码逻辑开发完成,下一步打包插件发布npm。

六、插件发布

如果没有自定义打包脚手架或者不想麻烦可以直接使用vue-cli配置进行插件打包发布。 1.封装好的插件进行导出,挂在到Vue上。

import VueVirtualAutoScroller from './components/vue-virtual-auto-scroller.vue';

const components = {
  VueVirtualAutoScroller,
};

function install(Vue) {
  const keys = Object.keys(components);
  keys.forEach((name) => {
    const component = components[name];
    Vue.component(component.name || name, component);
  });
}

export default {
  install,
  ...components,
};

2.配置package.json

{
  "name": "vue-virtual-auto-scroller",
  "version": "1.0.0",
  "author": "noah",
  "private": false,
  "description": "Infinite virtual scrolling high-performance vue plug-in that supports automatic scrolling",
  "main": "dist/vue-virtual-auto-scroller.umd.min.js",
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "vue-cli-service build --target lib --dest dist ./src/index.js"
  },
  "license": "ISC",
}

注意: scripts:打包target选用lib类库打包成umd格式 private: 插件发布要设置为false。 main: 插件入口文件一定要写对。

执行build命令生成以下文件代表你成功了

打包.png 3.添加适当的文档使用说明:

Props

  • dataArray: A list of items you want to display in the scroller.
  • containerHeight: Height of the scroller container.
  • autoScroll(default: true): Enables or disables auto-scrolling.
  • classOption(default: {itemHeight: 44, increment: 1, delay: 0}): Configuration for scroller styles.
    • itemHeight: Height of a single scroll element.
    • increment: Pixels scrolled per frame, not less than 1.

Features

  • Virtual Scrolling: Efficiently manages DOM rendering for large lists.
  • Auto-Scrolling: Automatically scrolls through items based on specified increments and delays.
  • Customizable Styles: Easily customize item heights and other scroll parameters through props.

执行下面命令上传到npm

npm login
npm publish

至此插件开发完成:www.npmjs.com/package/vue… 有业务需求的小伙伴请自行拿取。 截屏2024-06-27 下午1.56.43.png