Earlier this year, as I was looking through the results of a Lighthouse audit, I came across a recommendation to use passive event listeners. Since I didn’t know what this was referring to, I did a little investigation, and the following is what I discovered.

Waiting to Scroll

The issue this recommendation was addressing was the impact event listeners can have on scroll performance.

When you go to scroll a page and there’s a touch event listener (wheel, mousewheel, touchstart, touchmove, etc.), the browser will not scroll immediately until it knows that the listener isn’t going to prevent the default scroll behavior. It doesn’t want to start scrolling prematurely, only to find out the listener eventually calls preventDefault()) because it wants to do something different.

And so it hesitates, just to be sure. But these little hesitations can also lead to a janky scroll experience. What would be nice is for these event listeners to be able to let the browser know up front they aren’t going to change the default scroll behavior, thus allowing the browser to scroll immediately.

And that’s where passive event listeners come in.

By adding a {passive: true} flag to the event listener, we can now tell the browser the listener will NOT cancel the default scroll behavior, and it’s safe to scroll immediately. This feature is supported by Chrome 51+, Firefox 49+, Webkit, Edge 16+ (current support, and can make significant improvement to the performance of a site.

Here’s a video clip comparing a site with passive events vs. one without to highlight the potential differences.

How To Implement

The basic syntax to use this feature is as simple as:

document.addEventListener('touchstart', handler, {passive: true});

By passing in {passive: true} as the third argument, we’re indicating to the browser that the handler won’t disable scrolling via preventDefault().

Polyfill

As noted above, not all browsers support this, so it’s important to have a fallback for those that don’t—especially since older browsers use that third argument for a different purpose.

Here’s an example of a polyfill (source):

// 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("test", 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);

Use as Needed

If you don’t have any touch event listeners, then using passive ones won’t be necessary. But if you do have some, even if they themselves don’t do much, it’s important to make sure they’re marked as passive unless you truly intend to stop the default scrolling behavior.

Doing so is not difficult to do, and can add up to some big performance gains in the user experience.

Resources