You test your Rork app in the preview and everything looks great. Then you install the TestFlight build on your phone — and the animations are noticeably choppy.
This is one of those problems that tends to sneak up on indie developers right before launch. For a while, I assumed it was just "the nature of Rork-generated code" and moved on. But after actually digging into the root cause, I found that most animation jank in Rork apps comes down to a single missing line — and fixing it takes about ten seconds.
This article walks through how to diagnose the problem and, more importantly, how to fix it.
Why Animations Stutter: The JS Thread vs. UI Thread
React Native runs on two separate threads: the JS thread, which handles your JavaScript logic, and the UI thread, which handles screen rendering. Most animation code generated by Rork uses the Animated API, but by default, Animated performs its calculations on the JS thread.
The problem: when the JS thread gets busy — handling an API response, updating state, running a timer — animation calculations get delayed. To maintain 60fps, each frame needs to be computed within 16.67ms. If the JS thread is doing other work, frames get dropped and the animation stutters.
// ❌ Code Rork commonly generates — runs on JS thread, vulnerable to jank
const fadeAnim = useRef(new Animated.Value(0)).current;
Animated.timing(fadeAnim, {
toValue: 1,
duration: 300,
// useNativeDriver not specified — defaults to false, stays on JS thread
}).start();The UI thread, by contrast, runs independently of the JS thread. If you can offload animation calculations to the UI thread, your animations will stay smooth even when the JS thread is under load.
How to Diagnose Frame Drops
Before jumping to a fix, confirm that frame drops are actually happening. React Native includes a built-in Performance Monitor in development builds.
Open your app in the Expo development build on a real device, open the dev menu, and select "Show Performance Monitor." You'll see two live numbers: UI FPS and JS FPS.
What to look for:
- If
UI FPSis close to 60 butJS FPSis low — the JS thread is the culprit - If both values are low — there's a rendering bottleneck elsewhere (often in list views)
Also check the Rork Companion console for the warning: Animated: useNativeDriver was not specified. If this appears, the fix below will resolve it directly.
The Most Common Fix: Add useNativeDriver: true
This is the fix that handles the majority of cases. When useNativeDriver is set to true, animation calculations move from the JS thread to the UI thread.
// ✅ Fixed — animation runs on the UI thread, unaffected by JS thread load
const fadeAnim = useRef(new Animated.Value(0)).current;
Animated.timing(fadeAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true, // ← this single line makes the difference
}).start();
// Parallel animations: apply to each one individually
Animated.parallel([
Animated.timing(fadeAnim, { toValue: 1, duration: 300, useNativeDriver: true }),
Animated.spring(slideAnim, { toValue: 0, useNativeDriver: true }),
]).start();Important limitation: useNativeDriver: true only works with properties that don't affect layout — specifically opacity and transform (translateX, translateY, scale, rotate). It does not work with width, height, backgroundColor, or other layout properties.
// ❌ This will throw an error — width is not supported by the native driver
Animated.timing(widthAnim, {
toValue: 200,
duration: 300,
useNativeDriver: true,
}).start();
// ✅ Workaround: use transform.scaleX to simulate width changes
Animated.timing(scaleAnim, {
toValue: 2, // visually doubles the width without affecting layout
duration: 300,
useNativeDriver: true,
}).start();For Complex Animations: Switch to Reanimated
For scroll-linked animations, gesture-driven interactions, or anything more sophisticated, react-native-reanimated is the better long-term solution. It's already included in Expo's managed workflow.
// Scroll-linked fade-in using Reanimated
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
function AnimatedCard() {
const opacity = useSharedValue(0);
useEffect(() => {
opacity.value = withTiming(1, { duration: 400 });
}, []);
const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
}));
return (
<Animated.View style={[styles.card, animatedStyle]}>
{/* card content */}
</Animated.View>
);
}Reanimated's worklets run directly on the UI thread by default, completely bypassing the JS thread. For scroll-driven and gesture-driven animations, this makes a noticeable difference.
Fixing FlatList Scroll Jank
If your animations are fine but scrolling still stutters, the issue is usually in how FlatList items are rendered.
// ❌ Each scroll event triggers expensive re-renders
<FlatList
data={items}
renderItem={({ item }) => (
<View style={[styles.item, { backgroundColor: item.color }]}>
<Image source={{ uri: item.imageUrl }} />
<Text>{item.title}</Text>
</View>
)}
/>
// ✅ Memoize the item component to prevent unnecessary re-renders
const ItemComponent = React.memo(({ item }) => (
<View style={[styles.item, { backgroundColor: item.color }]}>
<Image source={{ uri: item.imageUrl }} />
<Text>{item.title}</Text>
</View>
));
<FlatList
data={items}
renderItem={({ item }) => <ItemComponent item={item} />}
windowSize={5} // limit off-screen rendering
initialNumToRender={8}
maxToRenderPerBatch={5}
/>Stutter Right After Navigation: Defer Heavy Work
If a screen transition itself stutters, the cause is often heavy data loading or initialization running during the transition animation. InteractionManager defers work until the animation completes:
import { InteractionManager } from 'react-native';
useEffect(() => {
const task = InteractionManager.runAfterInteractions(() => {
// Runs after the transition animation finishes
loadHeavyData();
});
return () => task.cancel();
}, []);In my own app, switching the list-to-detail transition to this pattern produced an immediately noticeable difference in perceived smoothness. Running heavy work in useEffect right at mount is a common shape in generated and hand-written code alike — for transition jank, suspect this first.
Prevent the Problem at the Prompt Level
The most efficient approach is to prevent the issue from appearing in the first place. When asking Rork to add animations, being explicit in your prompt makes a real difference:
"Add a fade-in animation. Use useNativeDriver: true and ensure the implementation maintains 60fps."
"For scroll-linked animations, use react-native-reanimated."
In my experience, explicit instructions like these consistently produce better output than relying on Rork's defaults — especially for more complex animation sequences where useNativeDriver tends to get omitted.
Pre-Release Checklist
After applying fixes, verify on a real device (not just the simulator — performance characteristics are very different):
- Performance Monitor shows both
UI FPSandJS FPSnear 60 - Animations remain smooth while scrolling
- No
useNativeDriver was not specifiedwarnings in the console - Acceptable smoothness on older hardware (iPhone XS equivalent)
Animation smoothness is one of those things users notice immediately, even if they can't articulate what's wrong. Most of the time, adding useNativeDriver: true is all it takes — so if something looks off, that's the first place to check. I hope this saves you the detour it took me.