Skip to content

Support passive scroll listeners #398

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
romulof opened this issue Sep 19, 2016 · 8 comments
Closed

Support passive scroll listeners #398

romulof opened this issue Sep 19, 2016 · 8 comments

Comments

@romulof
Copy link
Contributor

romulof commented Sep 19, 2016

Make Grid use passive scroll listeners (when available) for huge performance improvements.
Reference: https://developers.google.com/web/updates/2016/06/passive-event-listeners

Since React does not support passive event listeners (yet: facebook/react#6436), we have first to test for browser support using the following module (coded in es7 + flowtype):

/* ListenerOptions.js
 * @flow
 */
let passive: boolean = false;

const testOptions = {
  get passive() {
    passive = true;
  },
};

try {
  // $FlowIssue: addEventListener type hint does not support options yet
  document.addEventListener('test', null, testOptions);
} catch (e) {
  // ignore
}

export default {
  hasPassive: passive,
};

export function addPassiveEvent(element: HTMLElement, eventName: string, handler: EventHandler) {
  const options = passive ? { passive: true } : false;
  // $FlowIssue: addEventListener type hint does not support options yet
  element.addEventListener(eventName, handler, options);
}

Then instead of attaching using react's onScroll component prop, get the elements ref and add the event handler directly (or using the helper function in ListernerOptions).

/* @flow */
import React from 'react';
import { addPassiveEvent } from './ListenerOptions';


export default class PassiveScroller extends React.Component {
  onScrollerRef = (scrollerElement:HTMLElement) => {
    if (!scrollerElement) {
      if (this.scrollerElement) {
        // Prevent memory leaks
        this.scrollerElement.removeEventListener('scroll', this.onScroll);
      }

      this.scrollerElement = null;
      return;
    }

    this.scrollerElement = scrollerElement;
    addPassiveEvent(scrollerElement, 'scroll', this.onScroll);
  };

  onScroll:EventHandler = (event) => {
    // Normal scroll handler
  };

  scrollerElement: ?HTMLElement = null;

  render() {
    return (
      <div
        ref={this.onScrollerRef}
      >
        {this.props.children}
      </div>
    );
  }
}

I can submit the PR if there are no objections.

@romulof
Copy link
Contributor Author

romulof commented Sep 19, 2016

Browser support:

  • chrome: shipped in 51
  • firefox: shipped in 49
  • webkit: shipped in june: iOS 10 and Safari 10 (to be released)

@bvaughn
Copy link
Owner

bvaughn commented Sep 19, 2016

Hey @romulof! Thanks for the proposal. I'm definitely interested, if you'd be willing to put together a POC.

I have some initial concerns but we should test it out and see. My concerns are:

  • Using addEventListener handles things outside of React's awareness and prevents React from doing a more intelligent batching of setState. We would have to do this manually (or ensure we only call setState once) to prevent multiple renders.
  • In this case we would probably also want to use requestAnimationFrame but this can lead to some scrolling jitter in Safari (particularly) which we would have to be careful of. (I can elaborate more if we go that route. Just writing this down as a note for myself mostly.)

@romulof
Copy link
Contributor Author

romulof commented Sep 19, 2016

On standard scroll listeners the browser has to wait for the callback chain to finish before continuing the scroll, because someone might call event.preventDefault to block scrolling. Using passive scroll listeners the browser scrolls freely, independent of any JS, only informing the current scroll position.

One thing that is not obvious is the part of the life cycle when react calls your ref callback, but it's mostly covered in the snippet above.

I'm using these passive listeners in my current app and so far, so good. No problems with setState batching, but I'll read a little more on how React handles this and write down that POC.

And I didn't understand where requestAnimationFrame might be applied.

PS: Congrats for your library, it's very well written.

@bvaughn
Copy link
Owner

bvaughn commented Sep 19, 2016

Right, right. I understand the appeal of passive listeners and I think this could be a big performance win. I'd love to give it a try. 😁 I'm encouraged to hear that you're already using them in your app. That seems even more promising.

And I didn't understand where requestAnimationFrame might be applied.

May be unnecessary! 😄

Let's do it. I'd love to review a PR if you'd like to put one together.

And thank you! Much appreciated. 🙇

@bvaughn
Copy link
Owner

bvaughn commented Sep 21, 2016

Okay. I've got a branch locally where I've experimented with this a bit. Surprisingly, I don't see any improvements for my test harnesses (eg table.html and grid-test.html) using the Chrome Timeline view.

It's gotten late. I'll continue exploring this. Feel free to jump in and experiment with me if you'd like!

I've pushed a branch, avoid-unnecessary-reflows, where I made 2 changes:

  1. Updated AutoSizer (really the detect-element-resize util) to prevent it from swallowing "scroll" events.
  2. Try to cut down on unnecessary reflows while scrolling.

If you decide to jump in and help- consider starting from that branch. (Totally your call.)

Edit: The above changes have been merged to master and released as 8.0.6 FYI.

Your above options-support detection code looks fine. There's also the event-listener-with-options module, based on the WICG polyfill which might be a good option in terms of keeping react-virtualized as simple as possible (eg I hate the fact that I had to bundle the detect-element-resize script). But the options-support-detection approach is simple enough it doesn't matter either way.

@bvaughn
Copy link
Owner

bvaughn commented Sep 28, 2016

For the moment I am going to close this issue because I have not seen any positive performance impact. (I've actually seen some negative impact but I think that could be negated by batching my own setState operations the way React does for me when using the synthetic scroll event.)

I'll still keep the idea around. I think it might be worth revisiting in a couple of months.

Of course, if you find the time to experiment and find any different results than I did- let me know.

@bvaughn
Copy link
Owner

bvaughn commented Sep 28, 2016

By the way, I pushed my work-in-progress branch if you have any interest in checking it out:
https://github.com/bvaughn/react-virtualized/tree/passive-scrolling

It has very minimal changes:
https://github.com/bvaughn/react-virtualized/compare/passive-scrolling?expand=1

@taion
Copy link

taion commented Nov 27, 2018

Came across this issue while investigating an analogous issue on one of my repos. Note:

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants