如何解决长列表的滚动白屏问题
# 背景
之前提到过实现长列表的方式:监听 scroll 事件,在回调中计算渲染起止项并插入 dom 树,即只渲染可视区域
实现中发现,有些情况下可能会出现短暂的白屏现象
本文就来谈谈白屏成因和解决方案
# 白屏成因
造成滚屏有3种方式
- 输入事件,如鼠标滚轮,键盘方向键
- 拖动滚动条
- 代码控制 scrollTop
每种方式,浏览器的内部处理都是不一样的
# 输入事件滚动
先说交互事件,以鼠标滚轮(mousewheel)为例
大部分浏览器采用的是异步滚动模型。在该模型中,视觉滚动位置在合成器线程中更新,并在 scroll 回调执行前可见
内部执行顺序如下:
- 相关线程捕获到滚轮操作
- 通知合成器线程去滚动文档,包括更新滚动条位置
- 【事件循环1】执行滚轮事件回调
- 【事件循环1】执行 UI Render,触发 scroll 事件
- 【事件循环1】执行 scroll 事件回调
在执行「滚轮事件回调」的时候,文档位置可能已经更新过了。
因此,当 scroll 事件回调执行太久,就会出现文档已经滚动了,但是新的可视区域列表还未计算出来并更新到页面上,白屏就此产生。
我们可以禁用浏览器的异步滚动优化,即将「滚动文档操作」放到【事件循环1】的 UI Render 阶段去做
通过 passive=false 来实现 也就是说滚动文档需要与主线程交互
如果将原 scroll 事件的处理放到滚轮事件中处理的话, scrollTop 拿到的是之前的值(passive=false,浏览器并不知道是否要滚动,会不会被 prventDefault )。所以事件回调保持不变
这样下来,下一次的滚动文档必须等待本次 scroll 事件回调执行完毕,减缓了白屏现象,相应的,页面也显得没那么流畅
于是内部执行顺便变成如下:
- 相关线程捕获到滚轮操作
- 【事件循环1】执行滚轮事件回调
- 【事件循环1】执行 UI Render,触发 scroll 事件,通知合成器线程去滚动文档,包括更新滚动条位置
- 【事件循环1】执行 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 })
2
3
4
5
6
7
8
不过火狐中设置 passive: false
没有效果,浏览器的异步滚动优化无法禁用
详情看 Scroll-linked_effects (opens new window)
# 拖动滚动条
与滚轮事件有些许不同,chrome 的拖动滚动条没有异步滚动优化
其执行顺序如下:
- 相关线程捕获到滚动条被拖动
- 【事件循环1】执行 scroll 事件回调
- 【事件循环1】执行 UI Render,通知合成器线程去滚动文档,包括更新滚动条位置
效果就是卡一下,滚一下,滚过去的时候已经新的列表已经绘制完毕了,那么是不会有白屏问题的
document.addEventListener("scroll",function(e){
console.log(window.scrollY)
let start = performance.now()
// 模拟耗时任务
while( performance.now() - start <100){}
})
2
3
4
5
6
但是 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
2
3
4
5
6
7
8
发现 chrome 和 Firefox 此时的执行效果都一样,都是先执行 scroll 回调再滚动文档,没有什么异步滚动优化了。
这种情况下都不会出现白屏
# 解决方案
可以看到,chrome 的话只有输入事件可能导致白屏,且可以通过禁用滚动优化来减缓
而 Firefox 在输入事件和拖动滚动条的情况都会出现白屏,且基本不能解决
因此,我们只能提高 scroll 回调事件的执行效率,来减缓白屏的时长
目前有两个方向
- 算法优化
- 占位填充,通过防抖等滚动结束再计算
202309 补充:取消输入事件(鼠标滚轮、鼠标操作)默认行为,采用 JS 模拟滚动的方式,可以彻底解决。参考 精读《高性能表格》 (opens new window)
# 算法优化
具体可以参考我写的 前端长列表原理及优化 (opens new window) 一文
有两种策略:一种是可变滚动条总高度采用树状数组优化;一种是固定滚动条高度,通过定位项等几何关系算出
# 预填充+防抖
在 scroll 回调中先做填充,并对计算具体列表操作做防抖
效果上可能不太好,比较适合列表项带网络请求的情况,可以减少无效的网络请求
# 结语
因为很多都是浏览器自身的优化,不在规范范围内,本文较多结论是通过拓展阅读和实验结果得出,不保证正确。
欢迎指正~
# 拓展阅读
- https://hacks.mozilla.org/2016/02/smoother-scrolling-in-firefox-46-with-apz/
- https://developers.google.com/web/updates/2018/09/inside-browser-part4