Skip to content
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

Remove non-layout style and prop updates path via synchronouslyUpdatePropsOnUIThread #7014

Open
wants to merge 14 commits into
base: main
Choose a base branch
from

Conversation

tomekzaw
Copy link
Member

@tomekzaw tomekzaw commented Feb 11, 2025

Motivation

Currently, there are two ways to update native view props and styles in Reanimated. The default path (so-called slow path) is to apply all props changes to the ShadowTree via C++ API and let React Native mount the changes. However, if all props updated in given batch are non-layout props (i.e. those that don't require layout recalculation, like background color or opacity) we use a fast path that calls synchronouslyUpdatePropsOnUIThread from React Native and applies the changes directly to platform views, without making changes to ShadowTree in C++. Turns out, some features like view measurement or touch detection system use C++ ShadowTree which is not consistent with what's currently on the screen. Because of that, we're removing the fast path (turns out it's not that fast, especially on iOS) to restore the correctness of view measurement and touch detection for animated components.

Benchmarks

  • Performance monitor example → Bokeh Example
  • Android emulator / iPhone 14 Pro real device
  • Debug mode
  • Animating transform prop using useAnimatedStyle
Platform Before (main) After (this PR)
Android (count=200) 20 fps 15 fps
iOS (count=500) 22 fps 22 fps
App.tsx
import React, { useState } from 'react';
import { Dimensions, StyleSheet, View } from 'react-native';
import Animated, {
  Easing,
  useAnimatedStyle,
  useReducedMotion,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated';

const dimensions = Dimensions.get('window');

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

function Circle() {
  const shouldReduceMotion = useReducedMotion();

  const [power] = useState(randBetween(0, 1));
  const [duration] = useState(randBetween(2000, 3000));

  const size = 100 + power * 250;
  const width = size;
  const height = size;
  const hue = randBetween(100, 200);
  const backgroundColor = `hsl(${hue},100%,50%)`;

  const opacity = 0.1 + (1 - power) * 0.1;
  const config = { duration, easing: Easing.linear };

  const left = useSharedValue(randBetween(0, dimensions.width) - size / 2);
  const top = useSharedValue(randBetween(0, dimensions.height) - size / 2);

  const update = () => {
    left.value = withTiming(left.value + randBetween(-100, 100), config);
    top.value = withTiming(top.value + randBetween(-100, 100), config);
  };

  React.useEffect(() => {
    update();
    if (shouldReduceMotion) {
      return;
    }
    const id = setInterval(update, duration);
    return () => clearInterval(id);
  });

  const animatedStyle = useAnimatedStyle(
    () => ({
      transform: [{ translateX: left.value }, { translateY: top.value }],
    }),
    []
  );

  return (
    <Animated.View
      style={[
        styles.circle,
        { width, height, backgroundColor, opacity },
        animatedStyle,
      ]}
    />
  );
}

interface BokehProps {
  count: number;
}

function Bokeh({ count }: BokehProps) {
  return (
    <>
      {[...Array(count)].map((_, i) => (
        <Circle key={i} />
      ))}
    </>
  );
}

export default function App() {
  return (
    <View style={styles.container}>
      <Bokeh count={200} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: 'black',
    overflow: 'hidden',
  },
  circle: {
    position: 'absolute',
    borderRadius: 999,
  },
});

Summary

Test plan

@tomekzaw tomekzaw changed the title Remove synchronouslyUpdateUIPropsFunction_ call in performOperations Remove synchronouslyUpdatePropsOnUIThread path for non-layout prop updates Feb 12, 2025
@tomekzaw tomekzaw changed the title Remove synchronouslyUpdatePropsOnUIThread path for non-layout prop updates Remove non-layout style and prop updates path via synchronouslyUpdatePropsOnUIThread Feb 12, 2025
Copy link
Collaborator

@tjzel tjzel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, should we also merge/rework props lists in TS?

@tomekzaw
Copy link
Member Author

tomekzaw commented Feb 12, 2025

should we also merge/rework props lists in TS?

@tjzel We'll need to discuss this. There's no need to keep separate sets for UI and native props (since I've merged the logic of applying UI and native props) but I think we still need to expose addWhitelistedNativeProps and addWhitelistedUIProps for backward-compatibility of third-party libraries built on top on Reanimated, to some extent.

@tomekzaw tomekzaw marked this pull request as ready for review February 14, 2025 09:50
@tjzel
Copy link
Collaborator

tjzel commented Feb 14, 2025

@tomekzaw We can keep both these methods and mark one of them as deprecated.

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