◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-05-06Intermediate

Animation Jank in Rork Apps — How I Diagnosed Frame Drops and Fixed Them

Animations in your Rork app running smoothly in the preview but stuttering on a real device? This guide explains how to diagnose frame drops, why they happen, and how one line of code often fixes the problem completely.

Rork585React Native239Animation8Performance25useNativeDriverTroubleshooting43

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 FPS is close to 60 but JS FPS is 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 FPS and JS FPS near 60
  • Animations remain smooth while scrolling
  • No useNativeDriver was not specified warnings 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.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ Dev Tools2026-07-24
Collapsing Duplicate Requests Into One: A Reference-Counted Single-Flight Layer
When several components fire the same API call at launch, you get a burst of identical requests. Here is a single-flight layer that shares one in-flight promise instead: how to build the key that decides the folding, the trap of handing out a failed promise forever, the trap of one caller's abort cancelling everyone, and the test that keeps it all from regressing, with the real network numbers alongside.
⬡ Dev Tools2026-05-18
Fixing Linking.canOpenURL False Returns on iOS in Rork
Production iOS builds silently route every user to the mobile web instead of opening TikTok or X? This is almost always a missing LSApplicationQueriesSchemes entry. Here is the full fix path for Rork.
⬡ Dev Tools2026-05-02
When Your FlatList Starts Stuttering: Migrating Rork Apps to FlashList v2
When your Rork app's long lists start feeling sluggish, migrating to FlashList v2 makes scrolling dramatically smoother. Here is the practical migration path, taking advantage of v2's removal of estimatedItemSize.