如何控制绑定长耗时同步scroll事件的容器滚动行为
两种滚动表现的差异原因与控制方法
两种表现的核心差异取决于当前滚动是否运行在浏览器独立的合成器线程上。
两种场景的本质区别
- 场景1:滚动完全冻结
该场景为主线程滚动模式:滚动的位置计算、渲染更新与JS执行都在浏览器主线程处理。JS同步逻辑阻塞主线程时,滚动计算也会被同步阻塞,因此滚动表现为完全冻结,直到当前JS执行完成后才会更新滚动位置、触发下一次scroll事件。 - 场景2:滚动流畅但scroll事件延迟触发
该场景为合成器线程滚动模式:滚动的位置计算、页面渲染跑在独立的合成器线程,不受主线程JS阻塞的影响,因此滚动本身可以保持流畅。但scroll事件必须在主线程派发,因此会等当前阻塞主线程的JS执行完成后,才会按顺序派发攒下的scroll事件,表现为事件触发延迟。
主动控制场景的方案
强制触发「滚动完全冻结」的主线程滚动模式
只要触发任意一个让浏览器无法启用合成器滚动的条件即可:
- 给滚动容器或其子元素绑定
wheel、touchstart/touchmove等滚轮、触摸事件,且不设置passive: true参数(浏览器无法预判你是否会阻止默认滚动行为,会强制回退到主线程处理滚动) - 在滚动区域内使用需要主线程同步计算的CSS属性,比如
position: sticky、复杂配置的scroll-snap、依赖滚动位置的CSS变量等 - 旧版本浏览器默认所有非根元素的滚动都走主线程,无需额外配置即可复现该场景
你可以在你的示例代码里加一行测试,就能复现滚动冻结效果:
// 新增这行wheel事件绑定,不加passive参数即可触发主线程滚动 container.addEventListener('wheel', () => {})
强制触发「滚动流畅但事件延迟」的合成器滚动模式
需要排除所有阻塞合成器滚动的条件:
- 给所有绑定在滚动容器、滚动区域子元素上的
wheel、touch*事件都加上passive: true参数,明确告诉浏览器你不会阻止默认滚动行为 - 避免在滚动区域内使用需要主线程同步计算的滚动相关CSS属性,如果需要实现元素跟随滚动的效果,优先用CSS的
position: sticky代替JS动态修改位置 - 给滚动容器加上
will-change: scroll-position属性,提前提示浏览器对该区域的滚动做合成器优化
示例完整代码
<div id="container"> <div id="content"> <span id="text">Hola</span> </div> </div>
#container { height: 300px; width: 300px; border: solid 1px black; overflow: auto; /* 加这个属性提示浏览器优化合成器滚动 */ will-change: scroll-position; } #content { height: 1000px; width: 100%; background-image: linear-gradient(red, yellow); position: relative; } #text { position: absolute; left: 0; }
const container = document.getElementById("container"); const text = document.getElementById("text"); // 所有触摸滚轮事件都加passive: true,确保合成器滚动可用 container.addEventListener('wheel', () => {}, {passive: true}) container.addEventListener('scroll', (event) => { // 模拟耗时同步计算 let a = new Date().getTime(); while(new Date().getTime() < a + 1000){}; text.style.top = container.scrollTop + "px"; });
内容的提问来源于stack exchange,提问作者Xavier Morell Llorens
相关产品推荐
相关产品推荐

