software-mansion / react-native-reanimated

React Native's Animated library reimplemented
https://docs.swmansion.com/react-native-reanimated/
MIT License
8.87k stars 1.3k forks source link

Animating many views with worklets is significantly slower than with nodes #3541

Closed simontreny closed 3 months ago

simontreny commented 2 years ago

Description

I'm trying to animate 200 views using Reanimated 2.10.0 and I noticed that the animation is not smooth when I'm implementing it with worklets while it never drops a frame when I'm implementing it as node-based animation. The two implementations are pretty similar, with a single time animated value that drives the animation for all the animated views.

Here are two screenshots of the iOS simulator with React Native's perf monitor: Worklet (~42fps) Node (~60fps)

I ran the same code on a low-end Android phone in release mode with fewers views and I noticed the same performance issue. I understand that running worklets in their own JS VM is obviously slower than evaluating the result of a few animated nodes but I would not have expected such an impact on the performances. For now, I can fallback to the animated node version but I'm mostly worried that there will be no alternatives if Reanimated 3 drops support for animated nodes.

Steps to reproduce

Here is the code for the worklet animation:

import React, {useEffect, useRef} from 'react';
import {View} from 'react-native';
import Animated, {
  Easing,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated';

export const ConfettiLayerWorklet = () => {
  const time = useSharedValue(0);

  useEffect(() => {
    time.value = withTiming(10000000, {
      duration: 10000000,
      easing: Easing.linear,
    });
  });

  return (
    <View style={{flex: 1}}>
      {Array.from({length: 200}, (_, index) => (
        <Confetti key={index} time={time} />
      ))}
    </View>
  );
};

const Confetti = ({time}) => {
  const startX = useRef(random(0, 375)).current;
  const startY = useRef(random(0, 812)).current;
  const velocityX = useRef(random(0, 50) / 1000).current;
  const velocityY = useRef(random(70, 150) / 1000).current;

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [
      {translateX: (startX + time.value * velocityX) % 375},
      {translateY: (startY + time.value * velocityY) % 812},
    ],
  }));

  return (
    <Animated.View
      style={[
        {
          position: 'absolute',
          width: 20,
          height: 20,
          backgroundColor: 'blue',
        },
        animatedStyle,
      ]}
    />
  );
};

function random(min, max) {
  return min + Math.random() * (max - min);
}

And its counterpart using animated nodes:

import React, {useEffect, useRef} from 'react';
import {View} from 'react-native';
import Animated, {
  add,
  EasingNode,
  modulo,
  multiply,
  timing,
} from 'react-native-reanimated';

export const ConfettiLayerNode = () => {
  const time = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    const animation = timing(time, {
      toValue: 10000000,
      duration: 10000000,
      easing: EasingNode.linear,
    });
    animation.start();
    return () => animation.stop();
  });

  return (
    <View style={{flex: 1}}>
      {Array.from({length: 200}, (_, index) => (
        <Confetti key={index} time={time} />
      ))}
    </View>
  );
};

const Confetti = ({time}) => {
  const startX = useRef(random(0, 375)).current;
  const startY = useRef(random(0, 812)).current;
  const velocityX = useRef(random(0, 50) / 1000).current;
  const velocityY = useRef(random(70, 150) / 1000).current;

  const animatedStyle = {
    transform: [
      {
        translateX: modulo(add(startX, multiply(time, velocityX)), 375),
      },
      {translateY: modulo(add(startY, multiply(time, velocityY)), 812)},
    ],
  };

  return (
    <Animated.View
      style={[
        {
          position: 'absolute',
          width: 20,
          height: 20,
          backgroundColor: 'blue',
        },
        animatedStyle,
      ]}
    />
  );
};

function random(min, max) {
  return min + Math.random() * (max - min);
}

Snack or a link to a repository

https://github.com/simontreny/reanimated-perf-issue

Reanimated version

2.10.0

React Native version

0.69.5

Platforms

iOS

JavaScript runtime

JSC

Workflow

React Native (without Expo)

Architecture

Paper (Old Architecture)

Build type

Debug mode

Device

iOS simulator

Device model

Samsung Galaxy A5

Acknowledgements

Yes

szydlovsky commented 3 months ago

Just checked the example on 3.12 version and luckily it is running a perfectly stable 60fps 😄