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

[ScrollView] Use `onScroll` without event throttling #2228

Closed
aleclarson opened this Issue Aug 5, 2015 · 23 comments

Comments

Projects
None yet
@aleclarson
Copy link
Contributor

aleclarson commented Aug 5, 2015

I want to use the onScroll handler to setState another component in relation to the ScrollView's contentOffset changing its value.

But the throttling of onScroll causes noticeable jumps. Sometimes the component that has setState called on it will never even update until much later on.

Is there a better way to achieve what I want?

@ide

This comment has been minimized.

Copy link
Collaborator

ide commented Aug 5, 2015

Have you tried setting the throttling interval below 16?

@aleclarson

This comment has been minimized.

Copy link
Contributor Author

aleclarson commented Aug 5, 2015

@ide Yeah, it's at 15. The jittering persists no matter where I set it. I'm testing on an iPhone 5s.

@ide

This comment has been minimized.

Copy link
Collaborator

ide commented Aug 5, 2015

The jittering is likely due to the setState calls being too slow. If you replace the setState call with a console.log call does the performance noticeably improve?

@aleclarson

This comment has been minimized.

Copy link
Contributor Author

aleclarson commented Aug 5, 2015

I don't think I can easily test the performance by replacing setState with console.log since the only jittering is from the component I'm calling setState on.

Can I synchronize the rendering of my component and my ScrollView?

@ide

This comment has been minimized.

Copy link
Collaborator

ide commented Aug 5, 2015

It's hard to tell if performance improves when I take out the forced re-render of the component, no?

Well, yeah - my guess is that re-rendering is too slow.

Is there a way to batch my setState with the changing contentOffset, or is onScroll called after the ScrollView is "re-rendered"?

It's easier for me to explain the entire flow of events. When a native scroll event occurs on the main thread, it schedules a JS onScroll event for the next tick of the JS render loop (assuming no throttling) which runs every 16ms.

At the next tick on the JS thread, React runs your JS onScroll handler and your code calls setState which schedules a re-render before the end of the JS tick. At the end of the tick, the React subtrees with state changes are re-rendered and a JSON description of changes to the native views is sent back down to native. This all happens synchronously on the JS thread -- meanwhile the main thread is free to run UIKit code without being blocked by JS. But if your JS is too slow, that also means the main thread won't receive new updates in the meantime.

The native code processes the JSON description of changes on the shadow thread where it computes layout. Then it schedules the layout information to be applied to the native views at the next tick of the main thread. If you have a lot of expensive changes to the native views, this can take more than 16ms and your app will drop frames.

So basically if you've already configured the JS scroll view to throttle at 16ms or lower, you're already getting scroll events as often as possible. In fact you may want to increase the throttling so that you don't overload the system with too much work (React Native suffers from thundering herds atm).

@jeanregisser

This comment has been minimized.

Copy link
Contributor

jeanregisser commented Aug 5, 2015

I noticed the same thing when I implemented a parallax effect.
In my case onScroll was updating an animated value linked to the height of another component.
The lag in this configuration is very slight, maybe a frame or 2 when you scroll down really quickly before the height has caught up.

Not sure there's something that can be done for now.
Or we would need the ability to run some JS code on the main thread.

@yamill

This comment has been minimized.

Copy link
Contributor

yamill commented Aug 5, 2015

I think what you're looking for is something like this:

onScroll={Animated.event(
[{nativeEvent: {contentOffset: {y: this.state.yValue}}}]
)}

You can then animate your view using the state values & interpolate. I do this for a pull to refresh animation inside my renderHeader function:

<View style={{height: 100, justifyContent: 'center', alignItems: 'center', marginTop: -100}}>
              <Animated.View style={{
                backgroundColor: colors.white,
                width: 40,
                height: 40,
                borderRadius: 20,
                alignItems: 'center',
                justifyContent: 'center',
                opacity: this.state.yValue.interpolate({ inputRange: [-80,0], outputRange: [1, 0]}),
                transform: [
                  {scale: this.state.yValue.interpolate({ inputRange: [-80,0], outputRange: [1, .5]}),
                extrapolate: 'extend'}
                ]}}>
                <Text style={{color: colors.black, fontSize: 16}}>RCT</Text>
              </Animated.View>
 </View>
@browniefed

This comment has been minimized.

Copy link
Contributor

browniefed commented Sep 13, 2015

@ide @brentvatne Looks resolved here.

@brentvatne brentvatne closed this Sep 13, 2015

@marcshilling

This comment has been minimized.

Copy link

marcshilling commented Oct 29, 2015

@ide @brentvatne

I'm not sure this is resolved. I'm using the same tactic as @yamill to do animations and experiencing some very noticeable delays. The onScroll events simply do not come through the javascript bridge fast enough...

You can see the issue occurring in this gif: http://i.imgur.com/r15gMy1.gifv. The blurred background image is an Animated view that is having its height animated to always be aligned with the orange/gray bar, and its top animated to scroll off the screen as you scroll down. A console.log() in my render() method proves that this entire view is only being rendered once, therefore all the animations are being performed outside of the render loop. You can't notice it with gentle scrolling (beginning of the gif), but with fast scroll flicks (end of the gif) the problem rears its head real quick with the image noticeably snapping into the correct position after a big delay. My scrollEventThrottle is very low.

@zoharlevin

This comment has been minimized.

Copy link

zoharlevin commented Feb 14, 2016

@marcshilling

Did you find a solution to this?
I have a similar issue with a View I'm animating according to the scroll position. More or less like this: http://stackoverflow.com/questions/34389672/scroll-the-view-simultaneously-as-the-scrollview-scrolls

And there is a noticeable lag in the performance.

@aleclarson

This comment has been minimized.

Copy link
Contributor Author

aleclarson commented Feb 14, 2016

@marcshilling @zoharlevin Have either of you tested with your Xcode project set to Release instead of Debug?

If necessary, it would be nice to hear from Facebook about how to continue forward. 😄

@zoharlevin

This comment has been minimized.

Copy link

zoharlevin commented Feb 15, 2016

@aleclarson

I'm working on Android, not ios. I haven't tried a release apk yet, but I did try disabling JS Dev Mode which made no difference.
Also, as I understand the option for scrollEventThrottle is relevant only to ios.

Do you have any other suggestions I could try to improve the performance?

It seems like just a fraction of a second behind the actual scroll, but it's definitely noticeable. I wrote a demo app in native android which did the same thing with no lag at all, so I'm really eager to achieve the same level of performance in my react-native app.

@marcshilling

This comment has been minimized.

Copy link

marcshilling commented Feb 15, 2016

@zoharlevin nope, ended up changing our design requirements because we couldn't get the performance :(

@aleclarson yes, the problem is still apparent in a bundled release build.

Unless I'm missing something, I think this example is an example where React Native is just not gonna cut it performance-wise. Will have to drop down to a native scroll view.

@shendajht

This comment has been minimized.

Copy link

shendajht commented Mar 17, 2016

Resolved. Put both of the components which you want to sync their movement into a ScrollView. Btw, If you want to scroll one of the them in other direction, it also can be achieved, wrap the target by a View component. @marcshilling @aleclarson @jeanregisser @zoharlevin

@motiazu

This comment has been minimized.

Copy link

motiazu commented Mar 22, 2016

@shendajht What do you mean by put both components into a ScrollView?
I don't see how does that meet this requirement (being able to sync scroll position with something).
Can you provide a more concrete exmaple?

@aleclarson

This comment has been minimized.

Copy link
Contributor Author

aleclarson commented Mar 22, 2016

We'll have to wait for native drivers in AnimatedJS to be completed before we can achieve an Animated.Value that reacts to the scroll position without being delayed or losing frames.

The native⇄JS bridge cannot update the Animated.Value in response to the onScroll event without losing frames. There's a huge post about bridge performance in this post.

@motiazu

This comment has been minimized.

Copy link

motiazu commented Mar 24, 2016

@aleclarson Thanks for the article, I have a better understanding of the situation now.
And while this is the case, this issue should be open since there is no solution for it right now.

@alexprice1

This comment has been minimized.

Copy link

alexprice1 commented Jul 9, 2016

I am having a similar issue. Instead of throttling the event, onChange/scrollEventThrottle seem to pass me most of the events, just tremendously delayed ( like 15 seconds for the last events to come through after scrolling as completely stopped ). So it stopes scrolling, but I get several events for 15 seconds, and none of them are accurate on position until the last several seconds.

Any thoughts?

@thorbenandresen

This comment has been minimized.

Copy link

thorbenandresen commented Jul 27, 2016

@alexprice91 - hey, I also have problems with scrollEventThrottle, especially in DEBUG mode. It was defiantly a lot faster in RELEASE. I then also build my onw throttle, which worked for my usecase

   onScroll={(e) => {

                const currentScrollPos = e.nativeEvent.contentOffset.y
                const sensitivity = 50

                if (Math.abs(currentScrollPos - this.state.lastScrollPos) > sensitivity) {

                            this.props.doSomething()
                            this.setState({lastScrollPos: e.nativeEvent.contentOffset.y })
                      }
  } }

(I set lastScrollPos to 0 in the constructor() )

@alexprice1

This comment has been minimized.

Copy link

alexprice1 commented Jul 27, 2016

Got it, thank you! I forget about the latency involved with debugging in chrome, so thanks!

Sent from my iPhone

On Jul 27, 2016, at 3:37 PM, Thorben Andresen notifications@github.com wrote:

@alexprice91 - hey, I also have problems with scrollEventThrottle, especially in DEBUG mode. It was defiantly a lot faster in RELEASE. I then also build my onw throttle, which worked for my usecase

onScroll={(e) => {

            const currentScrollPos = e.nativeEvent.contentOffset.y
            const sensitivity = 50

            if (Math.abs(currentScrollPos - this.state.lastScrollPos) > sensitivity) {

                        this.props.doSomething()
                        this.setState({lastScrollPos: e.nativeEvent.contentOffset.y })
                  }

} }
(I set lastScrollPos to 0 in the constructor() )


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.

@mariodev12

This comment has been minimized.

Copy link

mariodev12 commented Apr 7, 2017

Same problem, anyone resolve this issue?

@motiazu

This comment has been minimized.

Copy link

motiazu commented Apr 9, 2017

@mariodev12 You can use Animated.event with native driver today.

From react native blog:

It also works with Animated.event, this is very useful if you have an animation that must follow the scroll position because without the native driver it will always run a frame behind of the gesture because of the async nature of React Native.

@GollyJer

This comment has been minimized.

Copy link

GollyJer commented Jul 7, 2017

Hey! I know this is old but I wanted to say thanks to those who provided insight here.
I've been trying to get a pinned header & footer solution working and synced scrolling was one of the key pieces. What I have works but isn't optimal.
If you want to follow along I put together a StackOverflow post that points to a working Expo Snack.

Thanks again!

@facebook facebook locked as resolved and limited conversation to collaborators Jul 22, 2018

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.