长列表的滚动白屏成因及解决方案

6,824 阅读4分钟

背景

之前提到过实现长列表的方式:监听 scroll 事件,在回调中计算渲染起止项并插入 dom 树,即只渲染可视区域

实现中发现,有些情况下可能会出现短暂的白屏现象

本文就来谈谈白屏成因和解决方案

白屏成因

造成滚屏有3种方式

  • 输入事件,如鼠标滚轮,键盘方向键
  • 拖动滚动条
  • 代码控制 scrollTop

每种方式,浏览器的内部处理都是不一样的

输入事件滚动

先说交互事件,以鼠标滚轮(mousewheel)为例

大部分浏览器采用的是异步滚动模型。在该模型中,视觉滚动位置在合成器线程中更新,并在 scroll 回调执行前可见

Smoother scrolling in Firefox 46 with APZ

内部执行顺序如下:

  1. 相关线程捕获到滚轮操作
  2. 通知合成器线程去滚动文档,包括更新滚动条位置
  3. 【事件循环1】执行滚轮事件回调
  4. 【事件循环1】执行 UI Render,触发 scroll 事件
  5. 【事件循环2】执行 scroll 事件回调

在执行滚轮事件回调的时候,可能文档位置已经更新完毕

因此,当 scroll 事件回调执行太久,就会出现文档已经滚动了,但是新的可视区域列表还未计算出来并更新到页面上,白屏就此产生

我们可以禁用浏览器的异步滚动优化,即将滚动文档操作放到【事件循环1】的 UI Render 阶段去做

通过 passive=false 来实现 也就是说滚动文档需要与主线程交互

如果将原 scroll 事件的处理放到滚轮事件中处理的话, scrollTop 拿到的是之前的值(passive=false,浏览器并不知道是否要滚动,会不会被 prventDefault )。所以事件回调保持不变

这样下来,下一次的滚动文档必须等待本次 scroll 事件回调执行完毕,减缓了白屏现象,相应的,页面也显得没那么流畅

于是内部执行顺便变成如下:

  1. 相关线程捕获到滚轮操作
  2. 【事件循环1】执行滚轮事件回调
  3. 【事件循环1】执行 UI Render,触发 scroll 事件,通知合成器线程去滚动文档,包括更新滚动条位置
  4. 【事件循环2】执行 scroll 事件回调
document.addEventListener("scroll",function(e){
  console.log(window.scrollY)
  let start = performance.now()
  // 模拟耗时任务
  while( performance.now() - start <100){}
})
document.addEventListener("mousewheel",function(e){
},{ passive: false })

不过火狐中设置 passive: false 没有效果,浏览器的异步滚动优化无法禁用

详情看 Scroll-linked_effects

拖动滚动条

与滚轮事件有些许不同,chrome 的拖动滚动条没有异步滚动优化

其执行顺序如下:

  1. 相关线程捕获到滚动条被拖动
  2. 【事件循环1】执行 scroll 事件回调
  3. 【事件循环1】执行 UI Render,通知合成器线程去滚动文档,包括更新滚动条位置

效果就是卡一下,滚一下,滚过去的时候已经新的列表已经绘制完毕了,那么是不会有白屏问题的

document.addEventListener("scroll",function(e){
  console.log(window.scrollY)
  let start = performance.now()
  // 模拟耗时任务
  while( performance.now() - start <100){}
})

但是 Firefox 做了滚动优化,且不能禁用

导致 scroll 回调会在滚屏后执行,和上面输入事件效果一致,于是就出现了白屏

代码控制

通过 dom.scrollTop=xxx 进行自动滚动

一般是用来回滚列表显示某一项的

document.addEventListener("scroll",function(e){
  console.log(window.scrollY)
  let start = performance.now()
  // 模拟耗时任务
  while( performance.now() - start <1000){}
})

document.scrollingElement.scrollTop=100

发现 chrome 和 Firefox 此时的执行效果都一样,都是先执行 scroll 回调再滚动文档,没有什么异步滚动优化了。

这种情况下都不会出现白屏

解决方案

可以看到,chrome 的话只有输入事件可能导致白屏,且可以通过禁用滚动优化来减缓

而 Firefox 在输入事件和拖动滚动条的情况都会出现白屏,且基本不能解决

因此,我们只能提高 scroll 回调事件的执行效率,来减缓白屏的时长

目前有两个方向

  1. 算法优化
  2. 占位填充,通过防抖等滚动结束再计算

算法优化

具体可以参考我写的 前端长列表原理及优化 一文

有两种策略:一种是可变滚动条总高度采用树状数组优化;一种是固定滚动条高度,通过定位项等几何关系算出

预填充+防抖

在 scroll 回调中先做填充,并对计算具体列表操作做防抖

效果上可能不太好,比较适合列表项带网络请求的情况,可以减少无效的网络请求

结语

因为很多都是浏览器自身的优化,不在规范范围内,本文较多结论是通过拓展阅读和实验结果得出,不保证正确。

欢迎指正~

2020/02/12 补充

scroll 回调的执行不是在下一轮事件循环中执行的,而是在上一轮的 UI Render 中执行的

拓展阅读

  1. hacks.mozilla.org/2016/02/smo…
  2. developers.google.com/web/updates…