我的长列表优化方案

背景

在实现说说列表功能时,只想到了实现具体业务,没有考虑到长列表对性能的影响,每个说说组件都包含了复杂节点结构,大量的事件处理程序和动画等,每当加载新的一页时会造成组件累积,占据大量内存,在安卓中的表现是滑动极其卡顿,在ios中的表现是加载2、3页左右会出现小程序崩溃的情况,当快速滑动时小程序会黑屏。

解决思路

导致崩溃、黑屏的原因经分析是因为内存占用过高,而导致内存占用过高的原因是因为说说组件累积,当加载新的一页时,前面的说说虽然已经不在需要,但是仍然会保留在内存中,对于移动端这种对内存限制要求高的设备会造成性能影响,因此最根本的办法是使用虚拟列表技术优化说说列表。
采用说说列表的核心思路是采用双数组的方式,一个数组存放所有说说,另一个数组存放需要被渲染的说说,以此来达到释放不再需要的说说组件的目的。但是要实现虚拟列表需要解决两个问题:
1. 怎么知道哪些说说组件应该被显示?哪些应该被释放? 2. 如何避免因前面的说说组件被释放而导致页面抖动的问题?

优化方案

第一次(页面滚动+偏移量计算)

思路
采用页面滚动监听+定位的方式实现。通过监听onPageScroll事件来获取滚动量,从而判断当前屏幕中应当出现哪些说说,但是这里有个问题每个说说组件的高度是不确定的,比如有些说说有9图片,高度就比普通说说高,因此不能直接计算说说的总高度与页面滚动量对比,这里的解决方法是为每个说说组件设置最小高度和偏移量,通过提前预判图片数量、文字个数来确定偏移量,也就是说说说的高度是最小高度+偏移量。
另外当前面说说组件释放后,由于页面流的作用,当前屏幕的说说会“塌陷上去”,造成窗口抖动,预想的解决方式是为第一条渲染说说设置定位,上面说说被释放的同时立刻计算第一条渲染说说应该出现的位置,然后通过position: relative + top来设置定位,避免“页面塌陷”。
结果
采用此方案后
通过定位来防止窗口抖动很难实现,因为非常难计算
onPageScroll事件触发太频繁,对性能消耗太大,此外在此事件进行大量计算可能会导致页面卡顿。
固定说说高度体验非常不好,而且偏移值难以计算,还容易出现bug,也不方便功能扩展。

第二次(空白盒子+节点查询)

思路
分析第一次失败的原因,原因有二:
1. 1.最小高度+偏移量来确定说说高度不具有可行性,首先是计算复杂,其次也不方便功能扩展。 2. 2.通过定位无法阻止窗口抖动,因为计算得来的高度很容易出现误差,哪怕与实际高度只有一点差距,都会造成窗口抖动。
因此此次优化思路应该寻找新的方法。阻止窗口抖动的解决思路是使用空白盒子(即只有宽高的div),空白盒子与被释放的说说高度相同,但是它只起占位置撑起页面的作用,对内存的占用可以忽略不计,说说组件被释放的同时将空白盒子替换上去,从而使页面不会出现抖动的情况。
要使空白盒子能够发挥作用,就必须要知道每个说说的实际高度(必须是真实高度,不能有误差),因此采用节点查询的方法,每次滑动时,都将所有说说渲染,然后通过节点查询获得元素真实高度并存放到一个全局数组中,这样就可以通过比较滚动量和说说元素的高度来确定哪些说说应该被渲染,哪些被释放。
总的来说这次的方法就是onPageScroll+空白盒子+节点查询
结果
实现解决内存过高的问题,但是性能优化效果不理想。
优点:
解决页面抖动的问题
不限制说说的高度,方便说说组件功能扩展
不需要复杂的计算
缺点:
onPageScroll仍然会频繁调用
需要额外的维护一个全局数组
说说需要先渲染一遍,获取元素高度后才能释放,虽然这样能解决内存堆积过高的问题,但是对性能仍然会有不小的影响。

第三次(区域级滚动+触碰定位)

思路
第二次的方案已经能够解决一定程度上的问题,但是仍然还不够好。分析第二次方案的缺点,最大的问题是需要获取说说元素的真实高度才能解决窗口抖动等问题,而追根溯源需要这样做的原因是因为前面两种方法都是通过比较说说高度和页面滚动量来判断哪些说说组件应该渲染,那能不能不计算直接找出应该被渲染的说说组件呢?
从项目整体上来分析,新校园助手小程序是主要受众是移动端用户,这与PC端有着很大的区别,我们前面的方法都是用浏览器的思维来考虑,没有考虑到移动端的特性,对于移动端来说,用户滑动就必定会触碰屏幕,从而触碰说说,而触碰到的那个说说组件的前后几个说说组件加上它自己就是我们要找的应该被渲染的说说组件。也就是说只要确定了用户滑动时触碰的那个说说,就能“定位”到所有应该被渲染的说说组件和应该被释放的说说组件。而确定用户滑动时触碰的那个说说的方法可以通过监听说说元素的touchstarttouchend等事件来实现,为了确定触碰的是哪个说说,还应该提前给说说进行编号。
另外从uniapp文档中可以得知:scroll-view组件自带防抖效果(overflow-anchor属性),因此我们在说说列表外层套上scroll-view组件。
结果
性能较之前有了很大的提升,但是页面还是偶尔会卡顿,还有优化的空间。
优点:
scroll-view组件自带防抖效果(overflow-anchor属性)
不需要额外维护全局数组,不需要先渲染所有说说,极大的提高了性能
不需要频繁地触发onPageScroll事件,提高性能
几乎不需要计算,维护相对简单,易扩展
组件提供相应的事件,比如自动滚动到某个位置
缺点
滑动较快会出现卡顿 -> 需要优化
如果用户快速上划,利用惯性滑动,就无法准确地判断哪些说说是应该渲染的,因为惯性滑动时用户没有触碰说说,用户可能利用惯性滑动到列表顶部,而系统却误以为用户只滑动一小段距离,只渲染上面的一小部分说说,导致更上面的说说应该被渲染但是却没有渲染出来。

第四次(说说组件优化)

思路
第三次的优化已经取得了很好的成绩,但是还需要解决页面滑动偶尔卡顿的问题。前面优化的关注点都在长列表优化上,这次优化将专注说说组件本身,通过优化DOM结构,使用事件代理,清理无用$watch、定时器,减少回流重绘等方法来提高性能。
此外还进行了用户体验优化。
优化清单
用户感知优化加载新的一页时(此时很容易卡顿),通过wx.showLoading提示用户正在加载并阻止用户继续滑动,知道新的一页加载出来。针对前面说的惯性上滑bug,增加手势上划和返回顶部按钮,在改变用户行为上解决bug(手动狗头)。
性能优化优化说说组件(最终目的减少组件渲染时间,实现方法是精简说说组件)使用事件代理删除无用watch、uni.$on等、优化DOM结构,原则上是让说说组件更加精简提取公共逻辑:将例如删除等逻辑提取出来,说说组件只需要触发事件即可减少回流图片加载有延迟,会造成回流通过预测图片的数量,固定图片容器的宽高,减少回流的影响。图片加载时离线,设好样式和宽高大小后再显示(通过v-show)
未解决的问题最大的问题仍未解决:即快速滑动的惯性效果会导致系统无法知道用户视口内是哪些说说滑动仍然有些卡顿

第五次(页面级滚动 + 定高空白盒子)

思路
经过第四次优化,说说列表滑动仍然会出现概率性卡顿,因此说明问题并不是出现在说说组件上,通过查询资料知道用scroll-view组件做长列表会有性能问题,因此本次优化方案将会把scroll-view局部区域级滚动改为页面级滚动,从而彻底解决页面滑动卡顿的问题。
不用scroll-view组件,那么如何解决页面抖动问题呢?答案是空白盒子+overflow-anchor:autooverflow-anchor:auto用来避免页面抖动(scroll-view组件自带防抖就是因为有这个属性)。空白盒子一方面可以用于当用户惯性滑动后如果“出界”了,触碰盒子后系统会重新“定位”出应该渲染的说说组件,一方面用于装饰,以免用户快速上划时一片空白。这次的空白盒子是固定高度,不需要是说说真实的高度了,因此也不用再进行节点查询。
优化清单
将scroll-view组件改为view,即将scroll-view滚动改为页面滚动,因为官方说明scroll-view做长列表会有性能问题,彻底解决滚动卡顿问题
overflow-anchor:auto; 让位置不随内容的变化而抖动
下滑的同时,在显示的说说上面添加空白盒子占位,从而解决惯性滑动导致列表"出界"的问题,同时还能通过用户触碰空白盒子来找到要渲染的说说组件。
结果
滚动卡顿问题被彻底解决了。
不能完全解决惯性滑动的问题,但是如果用户不触碰空白盒子,就无法正常显示说说,用户可能误以为是bug

最终方案

使用InterSelectionObserve API监听可见的盒子,如果盒子离开视口就会触发回调,在回调汇中停止监听该元素,并确定以该元素为基点增删上下两侧的盒子。