Что быстрее: Animated + useNativeDriver или Reanimated?

от автора

Привет! Меня зовут Денис, я мобильный разработчик в СберМаркете. Пишу на React Native и люблю анимации, ведь они дают жизнь нашим проектам 🙂

В этой статье попробуем разобраться, что же всё-таки работает быстрее: React-Native-Reanimated или Animated + useNativeDriver: true.

Будем сравнивать FPS, нагрузку на процессор, оперативную память и воспользуется EventQueue для получения логов.

Дисклеймер: Это теоретическое сравнение на абстрактных примерах, так что перфоманс на реальных проектах может отличаться. Но я постарался нагрузить инструменты как следует и хорошенько проверить их на прочность.

Почему именно этот эксперимент?

Эксперимент 1. Тестируем loop анимацию

Эксперимент 2. Добавляем scale

Эксперимент 3. Анимация, привязанная к скроллу

Так кто быстрее?

Почему именно этот эксперимент?

Не так давно мой коллега Женя Прокопьев (aka @Evgen175) опубликовал на Хабре статью в двух частях о том, почему анимации в React Native работают именно так, как работают. Цель серии — объяснить, откуда берутся лаги в анимациях, за которые многие так не любят RN, и дать советы, для каких случаев лучше использовать разные инструменты.

В финале получились следующие выводы:

Какой инструмент выбрать?

Animated

Reanimated

Для простых анимаций

Когда надо сделать анимацию, которая не перерисовывает макет: transform, opacity, borderRadius и др.

(!) Всегда надо использовать useNativeDriver: true

Если свойства, которые надо анимировать перерисовывают макет

Если надо реализовать сложную анимацию, где разные свойства зависят друг от друга

Когда используем жесты

А ещё появилась гипотеза:

Animated с useNativeDriver отправляет граф, описывающий анимацию, для расчета на UI, а Reanimated делает все расчеты на стороне JS.

Возможно из-за меньшего количества «накладных расходов» Animated будет иметь лучшую производительность и будет меньше потреблять память для тех кейсов, которые можно реализовать с помощью обоих инструментов.

Ну что ж, перехватываю эстафету. Хочется раз и навсегда выяснить, для каких кейсов что оптимальнее.

В красном углу ринга — Animated. В синем — Reanimated. Поехали!

Эксперимент 1. Тестируем loop анимацию

Возьмем 500 шариков с анимированным translateY, чтобы было нагляднее. Добавим loop и накинем easing функцию.

Код Reanimated
export const ReanimatedAnimatedItem = () => {   const animation = useDerivedValue(() =>     withRepeat(       withSequence(         withTiming(0, {           duration: twoNumRandom(1500, 2000),           easing: Easing.bounce,         }),         withTiming(1, {           duration: twoNumRandom(1500, 2000),           easing: Easing.bounce,         }),       ),       -1,       true,     ),   );    const animatedStyle = useAnimatedStyle(() => ({     transform: [       {         translateY: interpolate(           animation.value,           [0, 1],           [0, 834.3333129882812],         ),       },     ],   }));    return (     <Animated.View       style={[styles.box, {left: generateRandom()}, animatedStyle]}     />   ); }; 

Код Animated
export const NativeAnimatedItem = () => {   const value = useRef(new Animated.Value(0)).current;    useEffect(() => {     Animated.loop(       Animated.sequence([         Animated.timing(value, {           toValue: 834.3333129882812,           useNativeDriver: true,           duration: twoNumRandom(1500, 2000),           easing: Easing.bounce,         }),         Animated.timing(value, {           toValue: 0,           useNativeDriver: true,           duration: twoNumRandom(1500, 2000),           easing: Easing.bounce,         }),       ]),     ).start();   }, []);    const style = useMemo(     () => [       styles.box,       {         left: generateRandom(),         transform: [           {             translateY: value,           },         ],       },     ],     [],   );    return <Animated.View style={style} />; };

Получаем вот такой результат:

Сначала смотрим аниманию на Animated, затем на Reanimated

Сначала смотрим аниманию на Animated, затем на Reanimated

Animated

Reanimated

RAM

121

200

CPU(ios)

60%

60%

FPS

60

60

Оба инструмента справились неплохо, хотя в начале проявились небольшие просадки при построении виртуального дерева. Однако перфоманс монитор сверху показывает, что Reanimated требуется как минимум на 60мб больше оперативной памяти.

Нагрузка на ЦП в этом кейсе одинаковая.

Результаты первого эксперимента:

Плавность анимации (fps)

по очку Animated и Reanimated

Расходование памяти

победил Animated

Нагрузка на ЦП

по очку Animated и Reanimated

Animated 3-2 Reanimated

Первый раунд за Animated

Эксперимент 2. Добавляем scale

Добавим анимированное свойство scale. Посмотрим, как будет влиять на перфоманс наличие больше чем одного свойства.

Код Animated
export const NativeAnimatedItem = () => {   const value = useRef(new Animated.Value(0)).current;   const scallableValue = useRef(new Animated.Value(0)).current;    useEffect(() => {     Animated.loop(       Animated.sequence([         Animated.timing(scallableValue, {           toValue: twoNumRandom(0.85, 0.95),           useNativeDriver: true,           easing: Easing.bounce,         }),         Animated.timing(scallableValue, {           toValue: twoNumRandom(2.6, 3),           useNativeDriver: true,           easing: Easing.bounce,         }),       ]),     ).start();      Animated.loop(       Animated.sequence([         Animated.timing(value, {           toValue: 834.3333129882812,           useNativeDriver: true,           duration: twoNumRandom(1500, 2000),           easing: Easing.bounce,         }),         Animated.timing(value, {           toValue: 0,           useNativeDriver: true,           duration: twoNumRandom(1500, 2000),           easing: Easing.bounce,         }),       ]),     ).start();   }, []);    const style = useMemo(     () => [       styles.box,       {         left: generateRandom(),         transform: [           {             translateY: value,           },           {scale: scallableValue},         ],       },     ],     [],   );    return <Animated.View style={style} />; };

Код Reanimated
export const ReanimatedAnimatedItem = () => {   const scallableValue = useDerivedValue(() =>     withRepeat(       withSequence(         withTiming(twoNumRandom(0.85, 0.95)),         withTiming(twoNumRandom(2.6, 3)),       ),       -1,       true,     ),   );    const animation = useDerivedValue(() =>     withRepeat(       withSequence(         withTiming(0, {           duration: twoNumRandom(1500, 2000),           easing: Easing.bounce,         }),         withTiming(1, {           duration: twoNumRandom(1500, 2000),           easing: Easing.bounce,         }),       ),       -1,       true,     ),   );    const animatedStyle = useAnimatedStyle(() => ({     transform: [       {         translateY: interpolate(           animation.value,           [0, 1],           [0, 834.3333129882812],         ),       },       {         scale: scallableValue.value,       },     ],   }));    return (     <Animated.View       style={[styles.box, {left: generateRandom()}, animatedStyle]}     />   ); }; 

Animated

Reanimated

RAM

140

240

CPU(iOS)

181%

85%

FPS

2 (JS-поток)

60

useNativeDriver не справляется с нагрузкой, приложение просело до 1 кадра в секунду. А вот Reanimated показывает те же стабильные 60 fps. 

Нагрузка на процессор у Reanimated ниже чем у useNativeDriver, но оперативной памяти расходуется на 100 мб больше. Также, посмотрим на перфоманс монитор в Android Studio.

Perfomance монитор в android studio

Perfomance монитор в android studio

Для наглядности сравним расход оперативной памяти в кейсе с шариками на разном количестве элементов.

Существенная разница заметна лишь на 1000 нодах и выше. Но в реальности больше чем 100 анимированных нод встретить тяжело. В рамках эксперимента Reanimated берёт верх, но запомним, что преимущество здесь весьма умозрительное.

Результаты эксперимента:

Плавность анимации (fps)

победил Reanimated

Расходование памяти

победил Animated

Нагузка на ЦП

победил Reanimated

Animated 1-2 Reanimated

Второй раунд за Reanimated

Почему так произошло?

Давайте разбираться, почему эксперимент 1 и 2 дали разные результаты.

Не все методы поддерживают useNativeDriver. Вместо этого подключается requestAnimationFrame. Event.start() запускается в нативке, а вот остальные методы требуют лишний раз перегнать значение в JS-потоке.

В данном примере мы используем Sequence для объединения анимаций в цепочку. Из-за этого fps падает. Если убрать Sequence, то useNativeDriver будет включен и анимации станут более плавными.

В случае с Reanimated общение происходит через JSI, поэтому JS-поток не перегружается.

Давайте посмотрим в логи с помощью MessageQueue для большего понимания того что происходит под капотом:

Если кликнуть на картинку, будет проще прочитать :)

Если кликнуть на картинку, будет проще прочитать 🙂

В Reanimated создается таймер для анимации и сама нода. Никаких лишних вызовов не видим. Animated + useNativeDriver делает то же самое, но ещё каждый раз делает вызовы на выполнение анимации в JS-потоке.

Эксперимент 3. Анимация, привязанная к скроллу

Посмотрим на другой вид анимации — привязанный к скроллу. Для этого у Animated есть метод Event который создает карту анимированных значений. А в случае с Reanimated создадим shared value для получения позиции скролла. Анимировать будем Lottie анимацию.

Обзор на разные библиотеки, которые можно подключить к Reanimated можно прочитать ещё в одной статье Жени: Эффектно и эффективно. 6 инструментов для анимации в React Native

Код Reanimated
export const CompareWithScroll = () => {   const scrollY = useSharedValue(0);    return (     <Animated.ScrollView       style={{flex: 1, backgroundColor: '#fff'}}       onScroll={event => {         scrollY.value = event.nativeEvent.contentOffset.y;       }}       scrollEventThrottle={16}>       {new Array(100).fill(0).map(() => (         <LottieReanimated source={lottie} scrollY={scrollY} />       ))}     </Animated.ScrollView>   ); };  export const LottieReanimated = ({source, scrollY}: Props) => {   const animatedProps = useAnimatedProps(() => ({     progress: interpolate(-scrollY.value, [0, height], [0, 1]),   }));   const styles = useAnimatedStyle(() => ({     left: generateRandom(width),     top: generateRandom(height),   }));    return (     <AnimatedLottieView       style={[ownStyles.view, styles]}       source={source}       animatedProps={animatedProps}     />   ); };

Код Animated
export const CompareWithScroll = () => {   const scrollY = useRef(new Animated.Value(0));    return (     <Animated.ScrollView       style={{flex: 1, backgroundColor: '#fff'}}       onScroll={Animated.event(         [           {             nativeEvent: {               contentOffset: {y: scrollY.current},             },           },         ],         {useNativeDriver: true},       )}       scrollEventThrottle={16}>       {new Array(100).fill(0).map(() => (         <LottieNative source={lottie} scrollY={scrollY.current} />       ))}     </Animated.ScrollView>   ); };  export const LottieNative = ({source, scrollY}: Props) => {   const styles = useMemo(     () => [       {         left: generateRandom(width),         top: generateRandom(height),       },     ],     [],   );    return (     <AnimatedLottieView       style={[ownStyles.view, styles]}       source={source}       progress={scrollY.interpolate({         inputRange: [0, height],         outputRange: [0, 1],       })}     />   ); };

Теперь создадим сразу 100 элементов.

Animated

Reanimated

RAM

640

650

CPU(ios)

96%

97%

FPS

25-35

1

По оперативной памяти видим, что разницы практически нет, но Reanimated просел до 1 fps. Animated + useNativeDriver держится в пределах 20-30 fps.

В этом кейсе нагрузка на ЦП одинаковая.

Результаты эксперимента:

Плавность анимации (fps)

победил Animated

Расходование памяти

ничья

Нагузка на ЦП

ничья

Animated 3-2 Reanimated

Третий раунд за Animated

Так кто быстрее?

Animated победил, хоть и дал слабину во втором эксперименте. Оперативная память сильнее расходуется в Reanimated, а Animated + useNativeDriver сильнее нагружает процессор.

Итоговый результат:

эксперимент 1

эксперимент 2

эксперимент 3

Плавность анимации (fps)

R, A

R

A

Расходование памяти

A

A

R, A

Нагузка на ЦП

R, A

R

R, A

Animated 7-6 Reanimated

Animated ?

По факту же все зависит от контекста. Например, в эксперименте 2 Reanimated вырвал победу у Animated по плавности анимации при весьма нереалистичных для настоящего проекта предпосылках.

Итого, на небольших проектах можно в принципе не задумываться о скорости и использовать, что понравится. Reanimated покрывает почти все кейсы, но, в некоторых случаях, хуже по перфомансу. В то время как useNativeDriver имеет ограничения и позволяет анимировать только базовые свойства. Если нужно анимировать макет(width, height и т.д), то точно присматриваемся к Reanimated.

А вот и финальное сравнение инструментов:

Reanimated

Animated

+

Покрывает почти все кейсы

Удобный API

Идет из коробки

Большой размер исходников(3мб)

Ест много оперативной памяти

Ест ресурсы процессора

Не все свойства можно анимировать

Не все методы работают с useNativeDriver

Спасибо тем, кто дочитал до конца! Надеюсь, было полезно (и интересно). Если есть, что рассказать на тему производительности Animated и Reanimated, буду ждать вас в комментариях 🙂

Tech-команда СберМаркета ведет соцсети с новостями и анонсами. Если хочешь узнать, что под капотом высоконагруженного e-commerce, следи за нами в Telegram и на  YouTube. А также слушай подкаст «Для tech и этих» от наших it-менеджеров.


ссылка на оригинал статьи https://habr.com/ru/articles/765338/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *