dohooo / react-native-reanimated-carousel

🎠 React Native swiper/carousel component, fully implemented using reanimated v2, support to iOS/Android/Web. (Swiper/Carousel)
https://react-native-reanimated-carousel.vercel.app
MIT License
2.85k stars 329 forks source link

autoplay (in 4.0.0-canary.13) is FAR more processor-intensive; it should not use `requestAnimationFrame` on every call #669

Closed nmassey closed 2 months ago

nmassey commented 2 months ago

Describe the bug

  1. when autoplay is enabled, rather than using timers, the code in play() runs as quickly as it can on every animation frame. This uses a LOT of processor (see video screengrab below) and ultimately battery life
  2. autoplay now has different timings: subsequent scrolls do not wait for the scrollAnimationDuration time

How to fix

I recommend reverting:

Unfortunately, this PR seems to have a few issues:

  1. primarily, running this check code in requestAnimationFrame() over and over again is really awful for processor usage / performance / battery usage. Using setTimeout (or perhaps some other method for a timer?) would be better
  2. since lastTimestampRef.current = currentTimestamp is called immediately in play() after when calling next() and prev(), it restarts the countdown too soon (rather than waiting for the scroll to complete)

I'm not fully sure what #536 is intended to fix, but unfortunately it seems to add a few issues. ☹️

To Reproduce

<Carousel
  autoPlay={true}
  autoPlayInterval={1000}
  scrollAnimationDuration={1000}

Screengrab videos

4.0.0-alpha.12 (old code, better performance)

Notice that the CPU time stays low. It increases a little bit during scroll, but otherwise it stays low.

Also, see that it's almost exactly 2 seconds between transitions (1 second for the scrollAnimationDuration plus 1 second for autoPlayInterval)

https://github.com/user-attachments/assets/d1a3fe53-9d65-4c06-870e-f946634848c8

4.0.0-canary.13 (includes #536, worse performance)

Notice that the CPU time stays high. 😮

Also, notice that the time between transitions is not the same as in previous versions.

https://github.com/user-attachments/assets/2043ba01-ff6d-4881-b7b7-7a2c22f6b78b

Versions:

Smartphone:

dosubot[bot] commented 2 months ago

Hey there, @nmassey! I'm Dosu, a bot that can help you solve bugs, answer questions, and become a contributor. I'm here to assist you while waiting for a human maintainer.

I found a similar closed issue: Laggy performance with large data object (1000 items). This issue was resolved by a pull request: perf: reduce the amount of work done when rendering data [1].

To address the high processor usage and timing issues introduced in autoplay with requestAnimationFrame in react-native-reanimated-carousel version 4.0.0-canary.13, you can optimize the useAutoPlay hook by ensuring that requestAnimationFrame is used efficiently and that unnecessary re-renders or computations are minimized.

Here is a potential optimization for the useAutoPlay hook:

import { useEffect, useRef, useCallback } from 'react';

const useAutoPlay = ({
  autoPlay,
  autoPlayInterval,
  autoPlayReverse,
  carouselController,
}) => {
  const intervalRef = useRef(null);
  const frameRef = useRef(null);

  const startAutoPlay = useCallback(() => {
    if (!autoPlay) return;

    const play = () => {
      frameRef.current = requestAnimationFrame(() => {
        if (autoPlayReverse) {
          carouselController.prev();
        } else {
          carouselController.next();
        }
        intervalRef.current = setTimeout(play, autoPlayInterval);
      });
    };

    intervalRef.current = setTimeout(play, autoPlayInterval);
  }, [autoPlay, autoPlayInterval, autoPlayReverse, carouselController]);

  const pauseAutoPlay = useCallback(() => {
    if (intervalRef.current) {
      clearTimeout(intervalRef.current);
      intervalRef.current = null;
    }
    if (frameRef.current) {
      cancelAnimationFrame(frameRef.current);
      frameRef.current = null;
    }
  }, []);

  useEffect(() => {
    startAutoPlay();
    return () => pauseAutoPlay();
  }, [startAutoPlay, pauseAutoPlay]);

  return { start: startAutoPlay, pause: pauseAutoPlay };
};

export { useAutoPlay };

In this optimized version:

  1. requestAnimationFrame is used within a setTimeout to control the interval, ensuring that the animation frame is requested only when necessary.
  2. The startAutoPlay and pauseAutoPlay functions are memoized using useCallback to prevent unnecessary re-renders.
  3. The useEffect hook ensures that the autoplay starts when the component mounts and stops when it unmounts.

By implementing these changes, you should see a reduction in processor usage and improved timing accuracy for the autoplay functionality [2].

To continue talking to Dosu, mention @dosu.

nmassey commented 2 months ago

hi @dosu. Interesting. A few notes for you:

  1. you mentioned a similar closed issue (and the PR that resolved it). Apart from having mentioned "performance", this other issue does not appear to be related at all.
  2. when suggesting code, can you please suggest TypeScript for consistency with the code in this repository?
  3. overall, I think your suggested code is quite tight, and I like that. However, I have a few questions: a. what is the purpose of the requestAnimationFrame in the play() function? b. could startAutoPlay depend on carouselController.prev and carouselController.next instead of carouselController ? c. shouldn't the re-start after prev() and next() be in their onFinished callback arguments - like it was before #536 ?
dosubot[bot] commented 2 months ago
  1. The purpose of the requestAnimationFrame in the play() function is not applicable here because the play() function does not use requestAnimationFrame. Instead, it uses setTimeout to schedule the next slide transition.

  2. Regarding whether startAutoPlay could depend on carouselController.prev and carouselController.next instead of carouselController, the play() function already directly uses prev and next from carouselController. Therefore, startAutoPlay does not need to depend on carouselController itself but rather on its prev and next methods, which is already the case.

  3. As for whether the re-start after prev() and next() should be in their onFinished callback arguments like it was before #536, the current implementation already places the play() function in the onFinished callback of both prev and next:

timer.current = setTimeout(() => {
  autoPlayReverse
    ? prev({ onFinished: play })
    : next({ onFinished: play });
}, autoPlayInterval);

This ensures that play() is called again after each transition, maintaining the auto-play functionality [1].

To continue talking to Dosu, mention @dosu.

dohooo commented 2 months ago

@nmassey Hey, thanks for your detailed investigation. I thought about the same issues and also had another question. The requesAnimationFrame may have different performances on different devices. It's my carelessness merging this PR. Let's revert to this one. Thank you.

By the way, I'll investigate this later for the issue of autoPlay causing the animation delay on Android devices.

nmassey commented 2 months ago

looks like the following fixes this! Woohoo!!