
Привет! Меня зовут Денис, я мобильный разработчик в СберМаркете. Пишу на React Native и люблю анимации, ведь они дают жизнь нашим проектам :)
В этой статье попробуем разобраться, что же всё-таки работает быстрее: React-Native-Reanimated или Animated + useNativeDriver: true.
Будем сравнивать FPS, нагрузку на процессор, оперативную память и воспользуется EventQueue для получения логов.
Дисклеймер: Это теоретическое сравнение на абстрактных примерах, так что перфоманс на реальных проектах может отличаться. Но я постарался нагрузить инструменты как следует и хорошенько проверить их на прочность.
Почему именно этот эксперимент?
Эксперимент 1. Тестируем loop анимацию
Эксперимент 2. Добавляем scale
Эксперимент 3. Анимация, привязанная к скроллу
Почему именно этот эксперимент?
Не так давно мой коллега Женя Прокопьев (aka @Evgen175) опубликовал на Хабре статью в двух частях о том, почему анимации в React Native работают именно так, как работают. Цель серии — объяснить, откуда берутся лаги в анимациях, за которые многие так не любят RN, и дать советы, для каких случаев лучше использовать разные инструменты.
Под капотом анимаций в React Native. Часть 1/2: Animated and Bridge
Под капотом анимаций в React Native. Часть 2/2: Reanimated and JSI
В финале получились следующие выводы:
Какой инструмент выбрать? | |
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 | |
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.

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

Существенная разница заметна лишь на 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 | |
+ | Покрывает почти все кейсы | Идет из коробки |
– | Большой размер исходников(3мб) Ест много оперативной памяти | Ест ресурсы процессора Не все свойства можно анимировать Не все методы работают с useNativeDriver |
Спасибо тем, кто дочитал до конца! Надеюсь, было полезно (и интересно). Если есть, что рассказать на тему производительности Animated и Reanimated, буду ждать вас в комментариях :)
Tech-команда СберМаркета ведет соцсети с новостями и анонсами. Если хочешь узнать, что под капотом высоконагруженного e-commerce, следи за нами в Telegram и на YouTube. А также слушай подкаст «Для tech и этих» от наших it-менеджеров.
