RORK LABJP
PLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updatesPLAY — Google Play's target API level 36 requirement took effect yesterday, August 31. From today, new apps and updates must target Android 16VISIBILITY — Apps still on API 35 stay listed but disappear for users on newer Android versions. No error is raised; new installs simply fade, which makes the change easy to missEXTENSION — If you missed the deadline, an extension through November 1, 2026 can be requested in Play Console — best filed alongside a concrete migration planAPPLE — On the Apple side, the event lands September 9 and iOS 27 is reported to ship September 14. Testing generated apps on iOS 27 hardware before release week is time well spentEXPO — Expo released expo-paste-input on August 28, a native module that brings image, GIF, and sticker paste to React Native TextInputEAS — EAS Observe reached general availability on August 20, putting crash and performance monitoring on the same EAS platform as builds and updates
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.

Rork547React Native234Animation8Performance25useNativeDriverTroubleshooting39

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 $15 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-05-22
Why FlatList's onEndReached Fires Multiple Times — and How to Stop It
After wiring up infinite scroll in a Rork-generated FlatList, you may notice the same paginated request hitting your API two or three times in a row. Here's why onEndReached fires more often than you expect and how to add a two-layer defense that survives production.
Dev Tools2026-03-30
Rork × React Native Reanimated & Gesture Handler — Building 60fps Animations and Advanced Gesture Interactions
Keeping Reanimated 3 and Gesture Handler at 60fps, told through the production pitfalls I hit running wallpaper and calming-tone apps: delayed runOnJS, useDerivedValue caching, FlashList pairing, and missing cancelAnimation — each with real measurements.
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.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →