Eliminate Jank in React Native Animations with the Native Driver
Learn how to offload animation work to the UI thread using `useNativeDriver:true`, see a concrete button‑press example, and verify smooth 60 fps performance.
19 Dec 2025, 10:23 UTC

Why animations feel sluggish
When you animate a view with the React Native Animated API, the animation calculations run on the JavaScript thread by default. If that thread is busy handling gestures, network responses, or Redux updates, the UI thread does not receive new layout information in time. The result is dropped frames, commonly perceived as jank or a "stuttery" feel.
How the native driver solves the problem
Setting useNativeDriver: true tells the Animated library to serialize the animation description and send it to the UI (native) thread before the frame starts. Once on the UI thread, the animation proceeds independently of the JS thread, guaranteeing that each frame is delivered at roughly 16 ms as long as the animated properties are supported.
Supported properties include transform values (translateX, scale, rotate), opacity, and a subset of layout props like left and top. Anything else—such as backgroundColor or borderRadius—cannot be driven natively and will fall back to the JS driver unless you use a library like Reanimated 2.
Worked example: a press‑feedback button
The following snippet shows a button that fades in and scales up when pressed, using Animated.timing with the native driver enabled.
import React, { useRef } from 'react';
import { View, TouchableOpacity, Animated, Text, StyleSheet } from 'react-native';
const FeedbackButton = () => {
const anim = useRef(new Animated.Value(0)).current; // 0 → 1
const onPressIn = () => {
Animated.timing(anim, {
toValue: 1,
duration: 150,
useNativeDriver: true, // <-- key line
easing: Easing.out(Easing.cubic),
}).start();
};
const onPressOut = () => {
Animated.timing(anim, {
toValue: 0,
duration: 150,
useNativeDriver: true,
easing: Easing.out(Easing.cubic),
}).start();
};
const scale = anim.interpolate({
inputRange: [0, 1],
outputRange: [0.95, 1],
});
const opacity = anim.interpolate({
inputRange: [0, 1],
outputRange: [0.6, 1],
});
return (
Press me
);
};
const styles = StyleSheet.create({
button: {
backgroundColor: '#0066ff',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 6,
alignItems: 'center',
},
text: {
color: '#fff',
fontSize: 16,
},
});
// Note: backgroundColor and borderRadius are NOT animated here.
// If you need to animate them, you would have to omit useNativeDriver
// or use Reanimated 2.
Trade‑offs and limitations
- Property support: Only transform, opacity, and a few layout props work with the native driver. Animating unsupported props will silently ignore the flag and run on the JS thread, potentially re‑introducing jank.
- JS thread still needed for logic: Gesture handlers, state updates, and API calls remain on the JS thread. Heavy work there can still affect overall responsiveness.
- Hermes/JSI requirement: On older React Native versions (< 0.60) the native driver requires the legacy architecture. Enabling Hermes or using the new JSI‑based architecture ensures the driver is available.
Verifying that the native driver is active
Run the app on a physical device (emulators may not reflect real UI‑thread load). Open Flipper → Performance tab and watch the UI thread frame duration while pressing the button. You should see a steady ~16 ms per frame with no spikes.
Alternatively, inspect logcat:
adb logcat | grep Choreographer
If the native driver is working, you will see no lines containing "Skipped frames". The presence of such lines indicates the animation fell back to the JS thread.
Actionable closing
Start by identifying animations that only use transform or opacity. Wrap them with useNativeDriver: true and test on a device using Flipper or logcat. For any animation that requires unsupported properties, consider:
- Splitting the animation into a native‑driver part (transform/opacity) and a JS‑driven part (color, border).
- Adopting Reanimated 2, which lets you work around the property limitation while still staying on the UI thread.
By moving the eligible work off the JS thread, you’ll achieve smoother interactions and a more polished feel for your React Native app.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.