Search code examples
javascriptjquerygoogle-chromematerializelighthouse

Make touch and wheel event listeners added by libraries as "passive"


I have two projects:

  1. Bootstrap with jQuery
  2. materialize-css with Vanilla JS

When running a Lighthouse audit on both projects I used to get this warning caused by materialize-css on one project and jQuery on the other project:

enter image description here

I say "used to get", because I did manage to fix it for jQuery simply just applying this workaround:

const opts = (ns) => ... // some code deciding if browser supports passive
$.event.special.touchmove = { setup: function(_, ns, handle) { this.addEventListener('touchmove', handle, opts(ns)) } }
$.event.special.touchstart = { setup: function(_, ns, handle) { this.addEventListener('touchstart', handle, opts(ns)) } }
$.event.special.touchend = { setup: function(_, ns, handle) { this.addEventListener('touchend', handle, opts(ns)) } }

This seem to fix the issue for jQuery, I dont get such warning anymore and everything seems to be working fine.

Now, for materialize-css I found this package default-passive-events that (from docs):

It basically will set { passive: true } automatically every time you declare a new event listener.

Unfortunatelly this library did make materialize components break for touch events because of use of e.preventDefault()...

Is there a way, similar to jQuery workaround above, to fix all the materialize-css added event listeners? P.S. It does not use jQuery


Solution

  • First of all, this is just a warning, not an error.


    Is there a way, similar to jQuery workaround above, to fix all the materialize-css added event listeners? P.S. It does not use jQuery

    Yes, there are, actually, three ways:

    1. Get read of the warning, without improving performance

    Simply add { passive: false } as third param to all listeners which don't have an object as third parameter. This will tell browsers that .preventDefault() might be called on those events. However, especially on scroll, touchmove and touchstart events, the performance increase is considerable when the browser knows that the default behavior won't be prevented on an event. When marked as passive, the scrolling will be much smoother and the perceived performance will be significantly increased.

    1. Improve performance by potentially breaking functionality

    Add { passive: true } as third param to all listeners which don't have an object as third paramter. This will tell browsers that .preventDefault() will never be called on those events. You'll see a performance increase, but code relying on preventing those events will break.

    Note: this is what both the jQuery fix and the default-passive-events package do, btw.

    1. The proper way

    The proper way is to go into the source code of whatever lib you're fixing, figure out which events might ever be prevented and add { passive: false } for those, while adding { passive: true } for everything else.
    I'd argue finding all the places where an event is prevented in a lib is not a huge task.
    You can do this in a fork, ideally PR-ing it back into the lib's repo, for others to benefit, just like you benefit from the lib itself.


    Here's solution 1.

    function patchScrollBlockingListeners() {
      let supportsPassive = false;
      const x = document.createElement("x");
      x.addEventListener("cut", () => 1, {
        get passive() { supportsPassive = true; return !!1 }
      });
      x.remove();
      if (supportsPassive) {
        const originalFn = EventTarget.prototype.addEventListener;
        EventTarget.prototype.addEventListener = function(...args) {
          if (
            ['scroll', 'touchmove', 'touchstart'].includes(args[0]) &&
            (typeof args[2] !== 'object' || args[2].passive === undefined)
          ) {
            args[2] = {
              ...(typeof args[2] === 'object' ? args[2] : {}),
              passive: false
            };
          }
          originalFn.call(this, ...args);
        }
      }
    }
    patchScrollBlockingListeners();
    

    The above code only "patches" scroll, touchmove and touchstart events (by declaring them non-passive). this makes the warnings go away, without touching third party code.
    Note: in order for this to work, the function has to be run before loading whatever library is throwing the warning. The code above only patches events added after it was run, it doesn't patch already bound listeners.

    Note: Solution 2 is the same code, except the passive override is set to true.

    Another, rather important, note: while I can't guarantee that it will work for everyone, passing the following to passive has worked for me on "patching" a lot of libs in a lot of projects:

    passive: typeof args[2] === "boolean" ? args[2] : true
    

    It respects the previous addEventListener syntax (where 3-rd arg was passive itself, as boolean) and sets it to true when not specified at all. This, however will break events where the passive was not specified and the events are being canceled in some scenario, which is why I haven't included it above.