Mobile — App Performance Optimization
60 FPS Animations with Reanimated 3
Direct answer
Reanimated 3 achieves 60 FPS by running animation logic in worklets on the UI thread, so frames render even when the JavaScript thread is busy with React work. The core pattern is shared values updated by worklets and consumed by useAnimatedStyle, combined with Gesture Handler so touch tracking never round-trips through JS. The main way teams break this is calling back into JavaScript with runOnJS inside hot animation paths — keep per-frame logic entirely in worklets and reserve JS callbacks for animation completion.
Users cannot tell you your animation ran at 43 FPS, but they feel it instantly — the app reads as cheap. Reanimated 3 makes consistently smooth animation achievable in React Native, provided you respect the threading model it is built on. This is how I structure animation code that holds 60 FPS on real mid-range hardware.
Key facts, with sources
- The median crash-free session rate across mobile apps in 2025 was 99.95%, with top-performing teams at the 75th percentile holding 99.99%. (Luciq Mobile App Stability Outlook 2025)
- Compared with JavaScriptCore, the Hermes engine cuts typical React Native app startup from about 4.5 seconds to about 2.0 seconds, memory from about 185 MB to 136 MB, and engine app-size overhead from about 12 MB to 8 MB. (OneUptime Blog)
- Real-world production migrations to React Native's New Architecture report around 43% faster cold starts, 39% faster rendering, and 26% lower memory usage. (RapidNative)
- Shopify's performance bar for its React Native apps is critical screens loading in under 500 milliseconds at the 75th percentile, alongside over 99.9% crash-free sessions. (Shopify Engineering)
- Published optimization case studies include Discord cutting app startup time in half and Coinbase reporting an 80% funnel performance improvement after its React Native rewrite. (CatDoes)
Why animations drop frames in React Native
React Native splits work between the UI thread, which renders frames, and the JS thread, which runs your React code. An animation driven from the JS thread — updating state per frame, or computing values in JavaScript — competes with everything else that thread does: rendering components, processing responses, running effects. The moment a list re-renders or a payload parses mid-animation, frames drop, and it always happens during real usage rather than your clean demo.
The old escape hatch, the built-in Animated API with the native driver, offloads a limited set of properties but cannot run arbitrary logic per frame. Reanimated's answer is worklets: small functions compiled to run on the UI thread itself, so animation math executes next to rendering, indifferent to JS thread congestion.
The core pattern: shared values plus useAnimatedStyle
Almost everything in Reanimated 3 reduces to one loop: a shared value holds animated state accessible from both threads, a worklet mutates it, and useAnimatedStyle maps it to style properties on an Animated component. Wrap the target value in withSpring or withTiming and Reanimated drives the transition frame by frame on the UI thread — no React re-render is involved in producing frames.
Two habits keep this pattern fast. First, animate transform and opacity wherever possible; they compose on the GPU, while animating layout properties like width or top can force per-frame layout passes. Second, keep the useAnimatedStyle body small and pure — it runs every frame the style updates, so it is the worst possible place for allocation-heavy or branch-heavy logic.
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
function PressableCard({ children }: { children: React.ReactNode }) {
const pressed = useSharedValue(false);
const style = useAnimatedStyle(() => ({
transform: [{ scale: withSpring(pressed.value ? 0.96 : 1) }],
}));
return <Animated.View style={style}>{children}</Animated.View>;
}Gestures that stay glued to the finger
Gesture-driven UI — swipe to dismiss, draggable sheets, pull to refresh — is where threading discipline pays off most visibly. With Gesture Handler's Gesture API, callbacks like onChange run as worklets on the UI thread, so you can write touch deltas straight into shared values with zero JS round-trip. The element tracks the finger with no perceptible lag even while the JS thread is completely blocked.
Release behavior is where quality lives: hand the value to withSpring or withDecay on gesture end, using the gesture's velocity so motion continues naturally from the finger's speed rather than snapping to a canned duration. Users cannot articulate why this feels right, but side-by-side against a JS-driven drag, everyone picks it.
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
const offsetX = useSharedValue(0);
const pan = Gesture.Pan()
.onChange((e) => {
offsetX.value += e.changeX; // runs on the UI thread
})
.onEnd((e) => {
offsetX.value = withSpring(0, { velocity: e.velocityX });
});
const style = useAnimatedStyle(() => ({
transform: [{ translateX: offsetX.value }],
}));
// <GestureDetector gesture={pan}>
// <Animated.View style={style} />
// </GestureDetector>The runOnJS trap
Worklets cannot call ordinary JavaScript functions directly; runOnJS exists to schedule them back on the JS thread. Used at the right moments — navigating after a dismiss completes, committing state when a drag settles — it is exactly right. Used per frame, it silently rebuilds the problem Reanimated exists to solve: every frame now posts work to the congested thread, and your smooth worklet animation stutters whenever JS is busy.
In code audits I search for runOnJS inside onChange handlers and animation callbacks that fire continuously. The fix is usually moving the logic into the worklet — derived values, interpolation, clamping, and haptic triggers via worklet-capable libraries can all stay on the UI thread — and reserving runOnJS for the single moment an interaction ends.
Prove 60 FPS instead of assuming it
Animations get judged on the worst device your users own, so I validate on a mid-range Android phone in a release build — debug builds carry enough overhead to make smooth animations look broken and vice versa. Watch both threads: the performance monitor's JS and UI frame rates tell you whether drops come from your animation or from React work happening simultaneously, which changes the fix entirely.
The most valuable test is animation under load: trigger your transition while a list refreshes or a payload parses. Correctly workletized animations sail through; anything secretly depending on the JS thread stutters immediately. I also profile entering and exiting layout animations on long lists, where dozens of simultaneous animations can overwhelm cheap GPUs even with perfect threading — sometimes the honest fix is animating fewer things.
When to hire senior help
Bring in a senior performance specialist when your crash-free rate sits below roughly 99.9%, startup exceeds a couple of seconds on mid-range Android, or the team lacks profiling experience with Hermes, the JS thread, and native tooling. Performance rescue work is diagnostic in nature, so a short expert engagement typically finds the handful of hot spots that in-house teams spend months guessing at. If your stack includes React Native + Python + AI, a senior engineer who owns the full product beats coordinating multiple juniors.
Bottom line
Dhairya Senjaliya ships Mobile — App Performance Optimization projects worldwide — book a scoping call to discuss your specific situation.
Common pitfalls to avoid
- ✕Profiling only on high-end iPhones in dev mode, then discovering the real user base on low-end Android devices experiences multi-second startups and dropped frames
- ✕Rendering long feeds with ScrollView or an untuned FlatList instead of a virtualized list like FlashList, blocking the JS thread during scroll
- ✕Passing new inline functions and object literals on every render without memoization, causing cascading re-renders that never show up until lists grow
- ✕Ignoring JS bundle size and startup path, shipping no lazy loading or inline requires, so time-to-interactive balloons as the app grows
Frequently asked questions
Why are my Reanimated animations still dropping frames?
The usual causes: runOnJS being called every frame inside gesture or animation callbacks, animating layout properties like width or height that force per-frame layout instead of transform and opacity, heavy logic inside useAnimatedStyle, or testing in a debug build whose overhead masks true performance. Check the performance monitor — if the UI thread drops while JS is busy, something in your hot path still depends on JavaScript.
What is the difference between Reanimated and the built-in Animated API?
The built-in Animated API runs its driver logic on the JS thread unless you enable the native driver, which only supports a limited property set and no custom per-frame logic. Reanimated compiles worklets that execute arbitrary animation code directly on the UI thread, including gesture handling, interpolation, and spring physics, so animations stay smooth even when JavaScript is blocked by React rendering or data processing.
Should I use transform instead of animating width and height?
Yes, whenever the design allows. Transforms and opacity compose without recomputing layout, so they stay cheap at 60 FPS, while animating width, height, or position properties can trigger layout passes on every frame. For expand-and-collapse patterns, scale transforms or Reanimated layout transitions usually achieve the visual goal; reserve true layout animation for cases where surrounding content must genuinely reflow.
Which performance metrics should we actually track?
Crash-free session rate (the 2025 median is 99.95%, and below 99.8% is a red flag), cold start time, time-to-interactive, P75 screen-load time, and frame rate during scrolling. These need real-user monitoring in production, not just lab measurements.
Why is our React Native app slow on Android but fine on iOS?
The usual causes are testing only on flagship devices, JS-thread-blocking work, unvirtualized lists, and running an old React Native architecture or the JSC engine. Hermes and New Architecture migrations show measured gains of roughly 26 to 43% on memory and cold start, so upgrading the foundation is often the highest-leverage fix.
Does performance really affect revenue or is it an engineering vanity metric?
It compounds directly into acquisition and retention: crashes and slowness drive uninstalls and one-star reviews, and lower ratings measurably cut store conversion rates. That is why top consumer apps hold themselves to 99.99% crash-free sessions and sub-500ms screen loads.
Bottom line: Dhairya Senjaliya ships Mobile — App Performance Optimization projects worldwide. Book a scoping call at https://dhairyasenjaliya.com/#book-call.