How to Achieve 60FPS Animations in React Native

Nice and smooth animations are a crucial part of the success of your app. But, when done incorrectly, they can seriously damage your product by slowing it down. In this article, we describe some useful practices to achieve smooth 60FPS animations in React Native with no harm to the speed of your app.

The importance of smooth 60FPS animations on mobile devices

Mobile users are used to smooth and well-designed interfaces that quickly respond to their interactions and provide prompt visual feedback. As a result, applications have to register a lot of animations in many places that will have to run while other work is happening.

At the same time, the amount of information that can be passed over through the bridge is limited. There’s no built-in priority queue as of now. In other words, it is on you to structure and design your application so that both the business logic and animations can function without any disruptions. This is different from the way we are used to performing animations. For example, on iOS, the built-in APIs offer unprecedented performance and are always scheduled with the appropriate priority. Long story short – we don’t have to worry much about ensuring they’re running at 60 FPS.

With React Native, this story is a bit different. If you do not think about your animations top-down beforehand and choose the right tools to tackle this challenge, you’re on track to run into dropped frames sooner or later.

Janky animations make your app look slow and unfinished to users

In today’s sea of applications, providing a smooth and interactive UI might be one of your only ways to win over customers who are looking to choose the app to go.

If your application fails to provide a responsive interface that works well with the user interactions (such as gestures), not only may it affect new customers, but also decrease the ROI and user sentiment.

Mobile users like the interfaces that follow them along and that look top-notch, so ensuring the animations are always running smoothly is a fundamental part that builds such an experience.

One-off animations in React Native with native driver

Enabling the usage of native driver is the easiest way of quickly improving your animations' performance. However, the subset of style props that can be used together with the native driver is limited. You can use it with non-layout properties like transforms and opacity. It will not work with colors, height, and others. Those are enough to implement most of the animations in your app because you usually want to show/hide something or change its position.

const fadeAnim = useRef(new Animated.Value(0)).current;
const fadeIn = () => {
Animated.timing(fadeAnim, {
toValue: 1,
duration: 1000,
useNativeDriver: true, // enables native driver
}).start();
};
// [...]
<Animated.View style={{ opacity: fadeAnim }} />

For more complex use cases, you can use React Native Reanimated library. Its API is compatible with basic Animated library and introduces a set of fine-grained controls for your animations with a modern hooks-based interface. More importantly, it introduces the possibility to animate all possible style props with the native driver. So animating height or color will no longer be an issue. However, transform and opacity animations will still be slightly faster since they are GPU-accelerated. But regular users should not see any difference.

Ways to achieve gesture-driven 60FPS animations

The most desired effect that can be achieved with animations is being able to control animation with a gesture. For your customers, this is the most enjoyable part of the interface. It builds a strong sentiment and makes the app feel very smooth and responsive. Plain React Native is very limited when it comes to combining gestures with native driven animations. You can utilize ScrollView scroll events to build things like a smooth collapsible header.

For more sophisticated use cases, there is an awesome library, React Native Gesture Handler, which allows you to handle different gestures natively and interpolate those into animations. You can build a swipeable element by combining it with Animated. While it will still require JS callbacks, but there is a remedy for that!

The most powerful pair of tools for gesture-driven animations is using Gesture Handler combined with Reanimated. They were designed to work together and give the possibility to build complex gesture-driven animations that are fully calculated on the native side.

Reanimated API supports synchronous JavaScript execution on the UI thread using the concept of worklets. The library’s runtime spawns a secondary JS context on the UI thread that is then able to run JavaScript functions in the form of said worklets. Now using your imagination and leveraging Reanimated, you can create wonderful animations at full available speeds.

import React from 'react';
import { StyleSheet, View } from 'react-native';
import { PanGestureHandler } from 'react-native-gesture-handler';
import Animated, {
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withSpring,
} from 'react-native-reanimated';
const Snappable = (props) => {
const startingPosition = 0;
const x = useSharedValue(startingPosition);
const y = useSharedValue(startingPosition);
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: x.value }, { translateY:y.value }],
};
});
const eventHandler = useAnimatedGestureHandler({
onStart: (event, ctx) => {
ctx.startX = x.value;
ctx.startY = y.value;
},
onActive: (event, ctx) => {
x.value = ctx.startX + event.translationX;
y.value = ctx.startY + event.translationY;
},
onEnd: (event, ctx) => {
x.value = withSpring(startingPosition);
y.value = withSpring(startingPosition);
},
});
return (
<PanGestureHandler onGestureEvent={eventHandler}>
<Animated.View style={animatedStyle}>{props.children}</
Animated.View>
</PanGestureHandler>
);
};
const Example = () => {
return (
<View style={styles.container}>
<Snappable>
<View style={styles.box} />
</Snappable>
</View>
);
};
export default Example;
const BOX_SIZE = 100;
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
box: {
width: BOX_SIZE,
height: BOX_SIZE,
borderColor: '#F5FCFF',
alignSelf: 'center',
backgroundColor: 'plum',
margin: BOX_SIZE / 2,
},
});

Low-level handling of gestures might not be a piece of cake, but fortunately, there are third party libraries that utilize the mentioned tools and expose the prop callbackNode. It’s an Animated.Value that's derived from specific gesture behavior. Its value range is usually from 0 to 1, which follows the progress of the gesture. You can interpolate the values to animated elements on the screen. A great example of the libraries that expose CallbackNode are reanimated-bottom-sheet and react-native-tab-view.

import * as React from "react";
import { StyleSheet, Text, View } from "react-native";
import Animated from "react-native-reanimated";
import BottomSheet from "reanimated-bottom-sheet";
import Lorem from "./Lorem";
const { Value, interpolateNode: interpolate } = Animated;
const Example = () => {
const gestureCallbackNode = new Value(0);
const renderHeader = () => (
<View style={styles.headerContainer}>
<Text style={styles.headerTitle}>Drag me</Text>
</View>
);
const renderInner = () => (
<View style={styles.innerContainer}>
<Animated.View
style={{
opacity: interpolate(gestureCallbackNode, {
inputRange: [0, 1],
outputRange: [1, 0],
}),
transform: [
{
translateY: interpolate(gestureCallbackNode, {
inputRange: [0, 1],
outputRange: [0, 100],
}),
},
],
}}
>
<Lorem />
<Lorem />
</Animated.View>
</View>
);
return (
<View style={styles.container}>
<BottomSheet
callbackNode={gestureCallbackNode}
snapPoints={[50, 400]}
initialSnap={1}
renderHeader={renderHeader}
renderContent={renderInner}
/>
</View>
);
};
export default Example;
const styles = StyleSheet.create({
container: {
flex: 1,
},
headerContainer: {
width: "100%",
backgroundColor: "lightgrey",
height: 40,
borderWidth: 2,
},
headerTitle: {
textAlign: "center",
fontSize: 20,
padding: 5,
},
innerContainer: {
backgroundColor: "lightblue",
},
});

Giving your JS operations a lower priority for smoother animations

It is not always possible to fully control the way animations are implemented. For example, React Navigation uses a combination of React Native Gesture Handler and Animated which still needs JavaScript to control the animation runtime. As a result, your animation may start flickering if the screen you are navigating to loads a heavy UI. Fortunately, you can postpone the execution of such actions using InteractionManager.

import React, { useState, useRef } from "react";
import {
Text,
View,
StyleSheet,
Button,
Animated,
InteractionManager,
Platform,
} from "react-native";
import Constants from "expo-constants";
const ExpensiveTaskStates = {
notStared: "not started",
scheduled: "scheduled",
done: "done",
};
const App = () => {
const animationValue = useRef(new Animated.Value(100));
const [animationState, setAnimationState] = useState(false);
const [expensiveTaskState, setExpensiveTaskState] = useState(
ExpensiveTaskStates.notStared
);
const startAnimationAndScheduleExpensiveTask = () => {
Animated.timing(animationValue.current, {
duration: 2000,
toValue: animationState ? 100 : 300,
useNativeDriver: false,
}).start(() => {
setAnimationState((prev) => !prev);
});
setExpensiveTaskState(ExpensiveTaskStates.scheduled);
InteractionManager.runAfterInteractions(() => {
setExpensiveTaskState(ExpensiveTaskStates.done);
});
};
return (
<View style={styles.container}>
{Platform.OS === "web" ? (
<Text style={styles.infoLabel}>
❗InteractionManager works only on native platforms. Open example on
iOS or Android❗
</Text>
) : (
<>
<Button
title="Start animation and schedule expensive task"
onPress={startAnimationAndScheduleExpensiveTask}
/>
<Animated.View
style={[styles.box, { width: animationValue.current }]}
>
<Text>Animated box</Text>
</Animated.View>
<Text style={styles.paragraph}>
Expensive task status: {expensiveTaskState}
</Text>
</>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
paddingTop: Constants.statusBarHeight,
padding: 8,
},
paragraph: {
margin: 24,
fontSize: 18,
textAlign: "center",
},
box: {
backgroundColor: "coral",
marginVertical: 20,
height: 50,
},
infoLabel: {
textAlign: "center",
},
});
export default App;

This handy helper allows long-running work to be scheduled after any interactions/animations have completed. In particular, this allows JavaScript animations to run smoother.

Note: In the near future, you'll be able to achieve similar behavior with React itself on a renderer level (with Fabric) using the startTransition API.

In practice, you can show a placeholder, wait for the animation to finish, and then render the actual UI. It would help your JavaScript animations to run smoother and avoid interruptions by other operations. It's usually smooth enough to provide a great experience.

Remember that using JS-driven animations is bound to have significantly worse performance than native run on UI thread. Making it hard to achieve max FPS available for the device. Strive for platform-native animations whenever possible. And treat APIs such as InteractionManager for cases where heavy UI updates may interfere with gestures or animations. They might fight for the same resource-native UI thread.

Let users enjoy smooth animations and gesture-driven interface at 60FPS

There’s no one single right way of doing animations in React Native. The ecosystem is full of different libraries and approaches to handling interactions. The ideas suggested in this article are just recommendations to encourage you to not take the smooth interface for granted.

What is more important is painting that top-down picture in your head of all interactions within the application and choosing the right ways of handling them. There are cases where JavaScript-driven animations will work just fine. At the same time, there are interactions where native animation (or an entirely native view) will be your only way to make it smooth.

With such an approach, the application you create will be smoother and snappy. It will not only be pleasant to use, but also to debug and have fun with it while developing.

Insights

Learn more about Performance

cover

Jul 3Podcast

Why Nitro Is More Than a Faster Bridge

Marc Rousavy (Margelo) talks Nitro modules, native state, zero-copy data, Vision Camera, and why speed is only part of the story.

cover

May 25Article

Profile React Components in React Native Release Builds

Learn how to profile React components in React Native release builds with @callstack/inspector, using an Expo app setup that requires zero native code changes.

cover

Nov 21Article

Memory Ownership Models: When JavaScript Meets Native Code

A deep dive into how React Native handles memory when JavaScript and native code share ArrayBuffer data. Learn why async processing can break, how GC affects raw pointers, and the safe patterns for building high-performance native integrations.

cover

Oct 14Article

From Files to Buffers: Building Real-Time Audio Pipelines in React Native

Learn how to move beyond file-based audio APIs and work directly with in-memory PCM data in React Native. This guide shows how to construct WAV buffers, use JSI for zero-copy transfers, and enable real-time audio processing.

cover

Sep 8Article

How to Add Custom JSI Bindings in Your TurboModule (and Why You Might Need Them)

TurboModules provide a type-safe way to write native code in React Native. JSI sits underneath as a lower-level API, enabling direct interaction with the JavaScript runtime. Beyond what TurboModules expose, JSI gives you access to powerful features, such as working directly with ArrayBuffers. This article shows how to inject a custom binding step by step.

cover

Jul 24Article

Exploring Fibers: Cooperative Multitasking and Lock-Free Task Execution

Explore how fibers enable lightweight, cooperative multitasking. This guide walks through building a simple scheduler, managing task dependencies without locks, and using Tracy to visualize performance.

cover

May 21Article

Multithreading Isn’t Free: Performance Pitfalls Visualized

Learn how to profile and optimize multithreaded apps. Discover why more threads don’t always mean faster results and how bad thread contention really is.

Event Cover

Apr 28Webinar

Master JavaScript Performance in React Native (Part 2)

Learn to fix JS performance issues in React Native-from debounce bugs to Redux selectors, in this hands-on webinar with real code examples.

Events Cover

Apr 24Talk

Tips & Tricks From The Ultimate Guide to React Native Optimization 2025

View flattening, React 19 hooks, real DevTools tips-learn what actually helps your React Native app run faster.

Events Cover

Apr 24Talk

User-Focused Performance Tracking

Discover how Expensify defines and tracks performance metrics that actually impact user experience.

cover

Dec 18Podcast

React Native FastIO: Nitro-Powered Performance Boost

Listen to Mike Grabowski talk about React Native FastIO, a collection of primitives for working with data in React Native and its performance benefits.

cover

Oct 15Podcast

Superfast Nitro Modules With Marc Rousavy

Even faster React Native apps? That's possible with Nitro Modules! Tune in to the conversation with their creator, Marc Rousavy.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.