UX Notes

The 60fps rule: Why performance is a design problem

How we optimized a React Native app from 30fps to consistent 60fps by treating animation performance as a core design constraint, not an afterthought.

React Native performance profiling

Users don't forgive janky animations. A beautifully designed interface that stutters during scrolling feels broken, regardless of how good the underlying code is. Performance isn't a technical concern—it's a UX concern.

When we inherited a React Native app running at 30fps during basic interactions, the problem wasn't just "make it faster." The problem was that fundamental design decisions had made 60fps nearly impossible. Here's how we fixed it.

Why 60fps Matters

At 60fps, you have 16.67ms per frame. Miss that window and the animation stutters. Users immediately perceive anything below 50fps as laggy, even if they can't articulate why the app "feels slow."

The app we were optimizing had three critical performance issues:

  • Heavy JavaScript work on the UI thread during scrolling (parsing, filtering, sorting)
  • Complex animations that required constant JavaScript Bridge communication
  • Unnecessary re-renders cascading through deeply nested component trees

What Actually Fixed It

1. Move Everything Possible to the Native Thread

The biggest win came from using React Native Reanimated v2, which runs animations entirely on the native thread:


// Before: Runs on JS thread, bridges to native 60 times/second
const animatedValue = new Animated.Value(0);
Animated.timing(animatedValue, {
    toValue: 100,
    duration: 1000,
    useNativeDriver: true // Helps, but limited
}).start();

// After: Runs entirely on UI thread
const offset = useSharedValue(0);
const animatedStyle = useAnimatedStyle(() => {
    return {
        transform: [{ translateY: offset.value }]
    };
});

// Gesture fully handled on UI thread
const gesture = Gesture.Pan()
    .onChange((event) => {
        offset.value = event.translationY;
    });
                    

This single change improved scroll performance by 300%. Gestures that previously dropped to 25fps now stayed locked at 60fps.

2. Virtualize Everything Long

The app was rendering 500+ list items at once. Each scroll frame meant laying out 500 components. We switched to FlashList:


// Before: FlatList rendering everything
 }
/>

// After: FlashList with optimized rendering
 }
    estimatedItemSize={120}
    drawDistance={400}
/>
                    

FlashList's recycling strategy means we only render ~10-15 items at once, regardless of list length. Scroll performance went from 30fps to 60fps on lists with 1000+ items.

3. Memoize Aggressively

Every time a parent component re-renders, all children re-render unless explicitly memoized. We wrapped everything:


// Expensive component that shouldn't re-render unnecessarily
const ListItem = React.memo(({ item, onPress }) => {
    return (
         onPress(item.id)}>
            {item.title}
            {/* Complex rendering logic */}
        
    );
}, (prevProps, nextProps) => {
    // Custom comparison for deep equality
    return prevProps.item.id === nextProps.item.id &&
           prevProps.item.updatedAt === nextProps.item.updatedAt;
});

// Memoize callbacks to prevent child re-renders
const handlePress = useCallback((id) => {
    navigation.navigate('Detail', { id });
}, [navigation]);
                    

This reduced unnecessary renders by 80% and eliminated most frame drops during state updates.

4. Design Constraints as a Feature

The biggest breakthrough wasn't technical—it was convincing designers that some animations were too expensive:

  • Parallax scrolling with multiple layers? Replaced with simple fade transitions
  • Blur effects on scroll? Replaced with solid color overlays with opacity changes
  • Complex spring physics? Simplified to linear interpolation for most cases

The new designs looked just as good—sometimes better, because simplicity often beats complexity—and performed 5x better.

How We Measured Success

We used React Native's built-in performance monitor plus Flashlight for detailed profiling:


# Enable performance monitoring in dev
npx react-native start --reset-cache

# Profile with Flashlight
npx flashlight test --bundleId com.yourapp --testCommand "scroll-test"
                    

Key metrics we tracked:

  • JavaScript frame rate during scrolling (target: 60fps)
  • UI thread frame rate during gestures (target: 60fps)
  • Time to interactive on list screens (target: <500ms)
  • Memory usage during long scrolls (target: no leaks)

We tested on three devices: iPhone 12, Pixel 6, and a budget Android device (Samsung A32). If it hit 60fps on the A32, it would fly on everything else.

The Results

After two weeks of optimization work:

  • Scroll performance: 30fps → 60fps (stable)
  • Time to interactive: 1200ms → 400ms on list screens
  • JavaScript thread usage during scroll: 95% → 12%
  • Crash reports related to performance: 87% reduction

But the most important metric wasn't technical—user ratings mentioning "smooth" or "fast" increased by 34%, and complaints about "laggy" or "slow" dropped by 62%.

Key Takeaways

  1. Performance is a design constraint: If a design can't hit 60fps on mid-range hardware, change the design
  2. Profile on real devices: The simulator lies about performance
  3. Move work to native threads: Use Reanimated v2 for all animations and gestures
  4. Virtualize long lists: FlashList should be your default for any list over 20 items
  5. Memoize everything expensive: React.memo and useCallback aren't premature optimization in React Native—they're essential
  6. Question every animation: Simpler animations that hit 60fps beat complex animations that drop frames

Performance isn't something you bolt on at the end. It's a design principle that should inform every decision from day one. Users won't remember individual features, but they'll remember how your app feels—and smooth always wins.

Ready to build something amazing?

Let's discuss how we can help bring your product vision to life with thoughtful design and solid engineering.

Get Started