Lesson 8 of 8

Performance Optimization

Optimizing renders, lists, images, animations, and debugging performance

Performance in React Native

React Native apps can achieve near-native performance, but require attention to common pitfalls. The key areas are: rendering optimization, list performance, image handling, and smooth animations.

Avoiding Unnecessary Re-renders

import { memo, useCallback, useMemo } from 'react';
import { View, Text, FlatList, Pressable } from 'react-native';

// ❌ Bad: Creates new function on every render
function BadComponent({ items, onItemPress }) {
  return (
    <FlatList
      data={items}
      renderItem={({ item }) => (
        <Pressable onPress={() => onItemPress(item.id)}>
          <Text>{item.name}</Text>
        </Pressable>
      )}
    />
  );
}

// ✅ Good: Memoized component and callbacks
const MemoizedItem = memo(function Item({ item, onPress }) {
  return (
    <Pressable onPress={() => onPress(item.id)}>
      <Text>{item.name}</Text>
    </Pressable>
  );
});

function GoodComponent({ items, onItemPress }) {
  const handlePress = useCallback((id) => {
    onItemPress(id);
  }, [onItemPress]);
  
  const renderItem = useCallback(({ item }) => (
    <MemoizedItem item={item} onPress={handlePress} />
  ), [handlePress]);
  
  return (
    <FlatList
      data={items}
      renderItem={renderItem}
      keyExtractor={(item) => item.id}
    />
  );
}

// useMemo for expensive calculations
function ExpensiveComponent({ data }) {
  const processedData = useMemo(() => {
    return data.map(item => ({
      ...item,
      computedValue: expensiveCalculation(item),
    }));
  }, [data]);
  
  return <List data={processedData} />;
}

FlatList Optimization

import { FlatList, View } from 'react-native';

function OptimizedList({ data }) {
  const renderItem = useCallback(({ item }) => (
    <MemoizedListItem item={item} />
  ), []);
  
  // Estimate item height for faster scrolling
  const getItemLayout = useCallback((data, index) => ({
    length: 80, // item height
    offset: 80 * index,
    index,
  }), []);
  
  return (
    <FlatList
      data={data}
      renderItem={renderItem}
      keyExtractor={(item) => item.id}
      getItemLayout={getItemLayout}
      
      // Performance props
      removeClippedSubviews={true}     // Unmount off-screen items
      maxToRenderPerBatch={10}          // Items per batch
      updateCellsBatchingPeriod={50}    // ms between batch renders
      initialNumToRender={10}           // Initial items to render
      windowSize={5}                    // Render window multiplier
      
      // Maintain scroll position
      maintainVisibleContentPosition={{
        minIndexForVisible: 0,
      }}
    />
  );
}

// For very long lists, use FlashList
// npm install @shopify/flash-list
import { FlashList } from '@shopify/flash-list';

function FastList({ data }) {
  return (
    <FlashList
      data={data}
      renderItem={({ item }) => <ListItem item={item} />}
      estimatedItemSize={80}
    />
  );
}

⚡ FlashList vs FlatList

FlashList by Shopify is a drop-in replacement for FlatList that's up to 10x faster. It uses cell recycling (like native lists) instead of unmounting/remounting items.

Image Optimization

import { Image } from 'react-native';
import FastImage from 'react-native-fast-image';

// ❌ Bad: No size, no caching strategy
<Image source={{ uri: imageUrl }} />

// ✅ Good: Proper sizing and caching
<Image
  source={{ 
    uri: imageUrl,
    cache: 'force-cache',  // iOS only
  }}
  style={{ width: 200, height: 200 }}
  resizeMode="cover"
/>

// ✅ Better: Use FastImage for advanced caching
// npm install react-native-fast-image
<FastImage
  source={{
    uri: imageUrl,
    priority: FastImage.priority.normal,
    cache: FastImage.cacheControl.immutable,
  }}
  style={{ width: 200, height: 200 }}
  resizeMode={FastImage.resizeMode.cover}
/>

// Preload images
FastImage.preload([
  { uri: 'https://example.com/image1.jpg' },
  { uri: 'https://example.com/image2.jpg' },
]);

// For Expo, use expo-image
import { Image } from 'expo-image';

<Image
  source={imageUrl}
  style={{ width: 200, height: 200 }}
  contentFit="cover"
  placeholder={blurhash}
  transition={200}
/>

Animations with Reanimated

// npm install react-native-reanimated

import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  withTiming,
  interpolate,
  Easing,
} from 'react-native-reanimated';

function AnimatedBox() {
  const offset = useSharedValue(0);
  const scale = useSharedValue(1);
  
  // Animated styles run on UI thread (60fps)
  const animatedStyles = useAnimatedStyle(() => ({
    transform: [
      { translateX: offset.value },
      { scale: scale.value },
    ],
  }));
  
  const handlePress = () => {
    // Spring animation
    offset.value = withSpring(offset.value === 0 ? 100 : 0, {
      damping: 15,
      stiffness: 100,
    });
    
    // Timing animation
    scale.value = withTiming(scale.value === 1 ? 1.2 : 1, {
      duration: 300,
      easing: Easing.bezier(0.25, 0.1, 0.25, 1),
    });
  };
  
  return (
    <Pressable onPress={handlePress}>
      <Animated.View style={[styles.box, animatedStyles]} />
    </Pressable>
  );
}

// Scroll-based animations
function ParallaxHeader({ scrollY }) {
  const headerStyle = useAnimatedStyle(() => ({
    opacity: interpolate(scrollY.value, [0, 100], [1, 0]),
    transform: [
      { translateY: interpolate(scrollY.value, [0, 100], [0, -50]) },
    ],
  }));
  
  return <Animated.View style={[styles.header, headerStyle]} />;
}

Gesture Handling

// npm install react-native-gesture-handler

import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, { useSharedValue, useAnimatedStyle } from 'react-native-reanimated';

function DraggableBox() {
  const translateX = useSharedValue(0);
  const translateY = useSharedValue(0);
  const context = useSharedValue({ x: 0, y: 0 });
  
  const gesture = Gesture.Pan()
    .onStart(() => {
      context.value = { x: translateX.value, y: translateY.value };
    })
    .onUpdate((event) => {
      translateX.value = context.value.x + event.translationX;
      translateY.value = context.value.y + event.translationY;
    })
    .onEnd(() => {
      // Snap back or stay
    });
  
  const animatedStyle = useAnimatedStyle(() => ({
    transform: [
      { translateX: translateX.value },
      { translateY: translateY.value },
    ],
  }));
  
  return (
    <GestureDetector gesture={gesture}>
      <Animated.View style={[styles.box, animatedStyle]} />
    </GestureDetector>
  );
}

// Swipe to delete
function SwipeableItem({ onDelete }) {
  const translateX = useSharedValue(0);
  
  const gesture = Gesture.Pan()
    .onUpdate((e) => {
      translateX.value = Math.min(0, e.translationX);
    })
    .onEnd(() => {
      if (translateX.value < -100) {
        translateX.value = withTiming(-200);
        runOnJS(onDelete)();
      } else {
        translateX.value = withSpring(0);
      }
    });
  
  return (
    <GestureDetector gesture={gesture}>
      <Animated.View style={[styles.item, { transform: [{ translateX }] }]}>
        <Text>Swipe to delete</Text>
      </Animated.View>
    </GestureDetector>
  );
}

Debugging Performance

// Enable React DevTools Profiler
// In development, shake device > "Toggle Profiler"

// Monitor JS thread FPS
import { PerformanceMonitor } from 'react-native';

function App() {
  return (
    <>
      {__DEV__ && <PerformanceMonitor />}
      <MainApp />
    </>
  );
}

// Use Flipper for detailed profiling
// Install Flipper desktop app

// Detect slow renders with React DevTools
// Look for components with many renders

// Console timing
console.time('ExpensiveOperation');
// ... operation
console.timeEnd('ExpensiveOperation');

// Hermes profiling (production-like)
// npx react-native run-android --variant=release
// Then use Flipper's Hermes Debugger

Performance Checklist

Area Optimization
Lists Use FlatList/FlashList, keyExtractor, getItemLayout
Images Proper sizing, FastImage/expo-image, preloading
Components React.memo, useCallback, useMemo
Animations Reanimated (UI thread), native driver
Navigation Lazy loading screens, native stack
Bundle Hermes enabled, tree shaking, code splitting

⚠️ Common Performance Mistakes

  • • Creating functions/objects in render (use useCallback/useMemo)
  • • Using ScrollView for long lists (use FlatList)
  • • Animating with setState (use Reanimated)
  • • Large images without proper sizing
  • • Console.log in production builds
  • • Not using Hermes JavaScript engine