滚动事件:requestAnimationFrame VS requestIdleCallback VS 被动事件监听器

scroll events: requestAnimationFrame VS requestIdleCallback VS passive event listeners

正如我们所知,通常建议对滚动侦听器进行去抖动处理,以便在用户滚动时获得更好的用户体验。

但是,我经常发现 libraries and articles 像 Paul Lewis 这样有影响力的人推荐使用 requestAnimationFrame。然而,随着网络平台的快速发展,一些建议可能会随着时间的推移而被弃用。

我看到的问题是处理滚动事件的用例非常不同,例如构建视差网站或处理无限滚动和分页。

我看到 3 个可以在用户体验方面产生影响的主要工具:

所以,我想知道,每个用例(我只有 2 个,但你可以想出其他的),我现在应该使用什么样的工具来获得非常好的滚动体验?

更准确地说,我的主要问题会更多地与无限滚动视图和分页相关(通常不必触发视觉动画,但我们想要良好的滚动体验),是否更好地替换 requestAnimationFramerequestIdleCallback + 被动滚动事件处理程序的组合?我还想知道何时使用 requestIdleCallback 调用 API 或处理 API 响应以使滚动性能更好,或者浏览器可能已经处理了对我们来说?

虽然这个问题有点老了,但我想回答一下,因为我经常看到脚本,其中很多技术都被滥用了。

一般来说,您要求的所有工具(rAFrIC 和被动侦听器)都是很好的工具,不会很快消失。但是你必须知道为什么要使用它们。

开始之前:如果您生成滚动 synced/scroll 链接效果,如视差 effects/sticky 元素,使用 rICsetTimeout 进行节流没有意义,因为您想马上反应

requestAnimationFrame

rAF 在浏览器想要计算文档的新样式和布局之前为您提供框架生命周期内的点。这就是为什么它非常适合用于动画。首先,它不会比浏览器计算布局(正确频率)的调用频率更高或更低。其次,它在浏览器计算布局之前被调用(正确的时间)。事实上,使用 rAF 进行任何布局更改(DOM 或 CSSOM 更改)很有意义。 rAFV-SYNC 同步,就像浏览器中任何其他与布局渲染相关的东西一样。

对 throttle/debounce

使用 rAF

Paul Lewis 的默认示例如下所示:

var scheduledAnimationFrame;
function readAndUpdatePage(){
  console.log('read and update');
  scheduledAnimationFrame = false;
}

function onScroll (evt) {

  // Store the scroll value for laterz.
  lastScrollY = window.scrollY;

  // Prevent multiple rAF callbacks.
  if (scheduledAnimationFrame){
    return;
  }

  scheduledAnimationFrame = true;
  requestAnimationFrame(readAndUpdatePage);
}

window.addEventListener('scroll', onScroll);

这种模式经常出现 used/copied,尽管它在实践中几乎毫无意义。 (而且我在问自己为什么没有开发人员看到这个明显的问题。)一般来说,理论上将所有内容限制在至少 rAF 是很有意义的,因为请求布局更改没有意义来自浏览器的频率高于浏览器呈现布局的频率。

但是,每次浏览器 呈现 滚动位置更改时都会触发 scroll 事件。这意味着 scroll 事件与页面呈现同步。从字面上看,rAF 给你的是同样的东西。这意味着用某些东西来限制某些东西没有任何意义,根据定义,它已经被完全相同的东西限制了。

实际上你可以通过添加一个 console.log 来检查我刚才说的内容,并检查这种模式的频率 "prevents multiple rAF callbacks" (答案是 none,否则它会是浏览器错误) .

  // Prevent multiple rAF callbacks.
  if (scheduledAnimationFrame){
    console.log('prevented rAF callback');
    return;
  }

正如您将看到的,此代码从未执行过,它只是死代码。

但是有一个非常相似的模式出于不同的原因而有意义。看起来像这样:

//declare box, element, pos
function writeLayout(){
    element.classList.add('is-foo');
}

window.addEventListener('scroll', ()=> {
    box = element.getBoundingClientRect();

    if(box.top > pos){
        requestAnimationFrame(writeLayout);
    }
});

使用此模式,您可以成功减少甚至消除布局抖动。这个想法很简单:在你的滚动侦听器中,你读取布局并决定是否需要修改 DOM,然后你调用使用 rAF 修改 DOM 的函数。为什么这有帮助? rAF 确保您移动布局失效(在框架的末尾)。这意味着在同一框架内调用的任何其他代码都可以在有效布局上运行,并且可以使用超快速布局读取方法进行操作。

这个模式实际上非常棒,我建议使用以下辅助方法(用 ES5 编写):

/**
 * From 
 *
 * @param {Function} fn Callback function
 * @param {Boolean|undefined} [throttle] Optionally throttle callback
 * @return {Function} Bound function
 *
 * @example
 * //generate rAFed function
 * jQuery.fn.addClassRaf = bindRaf(jQuery.fn.addClass);
 *
 * //use rAFed function
 * $('div').addClassRaf('is-stuck');
 */
function bindRaf(fn, throttle) {
  var isRunning;
  var that;
  var args;

  var run = function() {
    isRunning = false;
    fn.apply(that, args);
  };

  return function() {
    that = this;
    args = arguments;

    if (isRunning && throttle) {
      return;
    }

    isRunning = true;
    requestAnimationFrame(run);
  };
}

requestIdleCallback

来自 API 类似于 rAF 但给出了完全不同的东西。它为您提供了帧内的一些空闲时间。 (通常是在浏览器计算布局并完成绘制之后,但在垂直同步发生之前还有一些时间。)即使从用户视图来看页面滞后,也可能有一些帧,浏览器是怠速。虽然 rIC 可以给你最大。 50 毫秒。大多数时候,您只有 0.5 到 10 毫秒的时间来完成您的任务。由于在框架生命周期中调用 rIC 回调的事实,您不应更改 DOM(为此使用 rAF)。

最后,使用 rIC 限制 scroll 用于延迟加载、无限滚动等的侦听器非常有意义。对于这些类型的用户界面,您甚至可以限制更多并在其前面添加 setTimeout。 (所以你等待 100 毫秒然后 rIC

Live examples for debounce and throttle.)

这里是 also an article about rAF,其中包含两个图表,可能有助于理解 "frame lifecycle" 内部的不同点。

被动事件侦听器

被动事件侦听器的发明是为了提高滚动性能。现代浏览器将页面滚动(滚动渲染)从主线程移动到组合线程。 (参见 https://hacks.mozilla.org/2016/02/smoother-scrolling-in-firefox-46-with-apz/

但是有些事件会产生滚动,这可以通过脚本来阻止(发生在主线程中,因此可以恢复性能改进)。

这意味着一旦这些事件侦听器之一被绑定,浏览器就必须等待这些侦听器被执行,然后浏览器才能计算滚动。这些事件主要是touchstarttouchmovetouchendwheel,理论上也有keypresskeydownscroll 事件本身 不是 这些事件之一。 scroll 事件没有默认操作,可以通过脚本阻止。

这意味着如果您不在 touchstarttouchmovetouchend and/or wheel 中使用 preventDefault,请始终使用被动事件侦听器,你应该没问题。

如果你使用 preventDefault,请检查你是否可以用 CSS touch-action 属性 替换它或至少在你的 DOM 树中降低它(例如这些事件没有事件委托)。如果有 wheel 个听众,您可以在 mouseenter/mouseleave.

上 bind/unbind 他们

对于任何其他事件:使用被动事件侦听器来提高性能没有意义。最重要的是要注意:scroll 事件无法取消,因此 永远scroll 使用被动事件侦听器是有意义的。

在无限滚动视图的情况下,您不需要 touchmove,您只需要 scroll,因此被动事件侦听器甚至不适用。

继续

回答你的问题

  • 对于延迟加载,无限视图对事件侦听器使用 setTimeout + requestIdleCallback 的组合,对任何布局写入(DOM 突变)使用 rAF
  • 对于即时效果仍然使用 rAF 进行任何布局写入(DOM 突变)。