滚动性能优化之 Passive Event Listeners 简介

4,969 阅读2分钟

现代浏览器基本上都具有 线程化滚动功能(a threaded scrolling feature ),即使在运行计算量昂贵的 JavaScript 时,滚动也可以顺畅地运行。但此优化可能由于需要等待 touchstarttouchmovetouchend 等事件处理程序的结果而失效。这可能原因是在 touch 事件处理函数中调用 preventDefault() 来完全阻止滚动。

singsong: 等待 touch 事件处理程序的结果(是否调用 preventDefault())而失效。如果调用了,完全阻止;如果没有调用了,导致滚动延迟开始。

addEventListener(
  document,
  'touchstart',
  function (e) {
    e.preventDefault(); // 完全阻止滚动
  },
  false
);

而浏览器这种阻止滚动行为其实是很少发生的,据统计表明:

大部分 touch 事件处理程序从未真正调用过 preventDefault(),因此浏览器通常会不必要阻止滚动。例如,在 Android 版 Chrome 浏览器中,有 80% 的阻止滚动的 touch 事件从未真正阻止过滚动(未调用 preventDefault())。这些事件中的 10% 给滚动的开始增加了超过 100ms 的延迟,其中 1% 增加至少 500ms 的延迟。

针对上述问题,网上提供了如下的优化方案:

  • 在 document 注册一个空的 touch 事件处理程序

    const touchHandler = function () {};
    document.addEventListener('touchmove', touchHandler);
    document.addEventListener('touchcancel', touchHandler);
    document.addEventListener('touchend', touchHandler);
    
  • 通过 CSS 样式 touch-action

    .classname {
      touch-action: none;
    }
    

然而这种问题不仅仅发生在 touch 事件上,wheel 事件也存在相同的问题。为了解决这个问题,WHATWG 引入了 Passive Event Listeners

Passive Event Listeners 是 DOM Spec 的新特性Chrome 51Firefox 49landed in WebKit 也开始实现了 Passive Event Listeners 特性。它允许开发人员明确 Touch EventWheel Event 是否需要阻止滚动,来优化滚动性能。

By marking a touch or wheel listener as passive, the developer is promising the handler won't call preventDefault to disable scrolling.

addEventListener(
  document,
  'touchstart',
  function (e) {
    e.preventDefault(); // 无效
  },
  { passive: true } // 明确告诉浏览器不会调用 preventDefault() 来阻止滚动
);

上述实例,调用了 e.preventDefault();,可能 console 面板打印如下提示信息:

Unable to preventDefault inside passive event listener due to target being treated as passive.

如果在 touchwheel 事件处理函数中调用 preventDefault(),同时没有开启 { passive: true }。此时可能 console 面板打印如下提示信息:

[Violation] Added non-passive event listener to a scroll-blocking ‘mousewheel’ event. Consider marking event handler as ‘passive’ to make the page more responsive.

对比优化效果视频

如何检测 passive

// Test via a getter in the options object to see if the passive property is accessed
var supportsPassive = false;
try {
  var opts = Object.defineProperty({}, 'passive', {
    get: function () {
      supportsPassive = true;
    },
  });
  window.addEventListener('testPassive', null, opts);
  window.removeEventListener('testPassive', null, opts);
} catch (e) {}

// Use our detect's results. passive applied if supported, capture will be false either way.
elem.addEventListener('touchstart', fn, supportsPassive ? { passive: true } : false);

参考文章