UTA DevHub

Performance Optimization

Building high-performance React Native components with optimization techniques

Performance Optimization

Overview

Performance is critical for React Native applications. This guide covers essential techniques for optimizing component performance, reducing re-renders, and ensuring smooth 60fps interactions.

Performance First Mindset

Poor performance in React Native manifests as:

  • Janky animations and scrolling
  • Slow screen transitions
  • Unresponsive touch interactions
  • High battery consumption

Always profile before optimizing!

Performance Implications

Understanding how React Native renders components is crucial:

  • JavaScript Thread: Runs your React code and business logic
  • UI Thread: Handles native UI rendering and user interactions
  • Bridge: Communication layer between JS and native
  • 60fps Target: ~16ms per frame for smooth interactions

Memoization Patterns

React.memo for Pure Components

Prevent unnecessary re-renders with React.memo:

// ❌ Without memo - re-renders on every parent update
const ExpensiveComponent: React.FC<Props> = ({ data, onPress }) => {
  console.log('Rendering ExpensiveComponent');
  return (
    <View>
      {data.map(item => (
        <Text key={item.id}>{item.name}</Text>
      ))}
    </View>
  );
};
 
// ✅ With memo - only re-renders when props change
export const ExpensiveComponent = React.memo<Props>(({ data, onPress }) => {
  console.log('Rendering ExpensiveComponent');
  return (
    <View>
      {data.map(item => (
        <Text key={item.id}>{item.name}</Text>
      ))}
    </View>
  );
});
 
// ✅ With custom comparison
export const ExpensiveComponent = React.memo<Props>(
  ({ data, onPress }) => {
    // Component implementation
  },
  (prevProps, nextProps) => {
    // Return true if props are equal (skip re-render)
    return (
      prevProps.data.length === nextProps.data.length &&
      prevProps.data.every((item, index) => item.id === nextProps.data[index].id)
    );
  }
);

useMemo for Expensive Computations

Cache computed values between renders:

const ProductList: React.FC<ProductListProps> = ({ products, filters }) => {
  // ❌ Recalculates on every render
  const filteredProducts = products.filter(product => {
    return filters.every(filter => filter.matches(product));
  });
 
  // ✅ Only recalculates when dependencies change
  const filteredProducts = useMemo(() => {
    console.log('Filtering products');
    return products.filter(product => {
      return filters.every(filter => filter.matches(product));
    });
  }, [products, filters]);
 
  // ✅ Complex calculations
  const statistics = useMemo(() => {
    return {
      total: filteredProducts.length,
      avgPrice: filteredProducts.reduce((sum, p) => sum + p.price, 0) / filteredProducts.length,
      categories: [...new Set(filteredProducts.map(p => p.category))],
    };
  }, [filteredProducts]);
 
  return (
    <View>
      <Statistics {...statistics} />
      <FlatList
        data={filteredProducts}
        renderItem={({ item }) => <ProductCard product={item} />}
      />
    </View>
  );
};

useCallback for Stable References

Prevent child re-renders by maintaining stable function references:

const ParentComponent: React.FC = () => {
  const [selectedId, setSelectedId] = useState<string | null>(null);
 
  // ❌ Creates new function on every render
  const handleSelect = (id: string) => {
    setSelectedId(id);
  };
 
  // ✅ Stable function reference
  const handleSelect = useCallback((id: string) => {
    setSelectedId(id);
  }, []); // Empty deps because setState is stable
 
  // ✅ With dependencies
  const handleSelectWithLogging = useCallback((id: string) => {
    console.log(`Previous: ${selectedId}, New: ${id}`);
    setSelectedId(id);
  }, [selectedId]);
 
  return (
    <FlatList
      data={items}
      renderItem={({ item }) => (
        <MemoizedItem
          item={item}
          isSelected={item.id === selectedId}
          onSelect={handleSelect}
        />
      )}
    />
  );
};

Lazy Loading Patterns

Component Code Splitting

Split large components to reduce initial bundle size:

// ❌ Loading everything upfront
import { HeavyComponent } from './HeavyComponent';
import { AnalyticsView } from './AnalyticsView';
import { AdminPanel } from './AdminPanel';
 
// ✅ Load on demand
const HeavyComponent = lazy(() => import('./HeavyComponent'));
const AnalyticsView = lazy(() => import('./AnalyticsView'));
const AdminPanel = lazy(() => import('./AdminPanel'));
 
const App: React.FC = () => {
  const [showAnalytics, setShowAnalytics] = useState(false);
 
  return (
    <View>
      <Suspense fallback={<LoadingSpinner />}>
        {showAnalytics && <AnalyticsView />}
      </Suspense>
    </View>
  );
};

Progressive Loading

Load content progressively for better perceived performance:

const ProgressiveScreen: React.FC = () => {
  const [phase, setPhase] = useState<'critical' | 'important' | 'nice-to-have'>('critical');
 
  useEffect(() => {
    // Load critical content immediately
    const timer1 = setTimeout(() => setPhase('important'), 100);
    const timer2 = setTimeout(() => setPhase('nice-to-have'), 500);
 
    return () => {
      clearTimeout(timer1);
      clearTimeout(timer2);
    };
  }, []);
 
  return (
    <ScrollView>
      {/* Critical content - render immediately */}
      <Header />
      <MainContent />
 
      {/* Important but not critical */}
      {phase !== 'critical' && (
        <Suspense fallback={<Skeleton height={200} />}>
          <RelatedContent />
        </Suspense>
      )}
 
      {/* Nice to have - load last */}
      {phase === 'nice-to-have' && (
        <Suspense fallback={<Skeleton height={100} />}>
          <Recommendations />
        </Suspense>
      )}
    </ScrollView>
  );
};

Virtualization Patterns

Optimized FlatList

Configure FlatList for optimal performance:

const OptimizedList: React.FC<Props> = ({ data }) => {
  // Stable references for props
  const keyExtractor = useCallback((item: Item) => item.id, []);
  
  const getItemLayout = useCallback(
    (data: ArrayLike<Item> | null | undefined, index: number) => ({
      length: ITEM_HEIGHT,
      offset: ITEM_HEIGHT * index,
      index,
    }),
    []
  );
 
  const renderItem = useCallback(({ item }: { item: Item }) => (
    <MemoizedItemComponent item={item} />
  ), []);
 
  return (
    <FlatList
      data={data}
      renderItem={renderItem}
      keyExtractor={keyExtractor}
      getItemLayout={getItemLayout}
      
      // Performance optimizations
      removeClippedSubviews={true}
      maxToRenderPerBatch={10}
      updateCellsBatchingPeriod={50}
      windowSize={10}
      initialNumToRender={10}
      
      // Avoid inline functions
      ItemSeparatorComponent={ItemSeparator}
      ListEmptyComponent={EmptyComponent}
      ListFooterComponent={FooterComponent}
    />
  );
};
 
// Memoized item component
const ItemComponent = memo<{ item: Item }>(({ item }) => {
  return (
    <View style={styles.item}>
      <Text>{item.title}</Text>
      <Text>{item.description}</Text>
    </View>
  );
});
 
// Pre-defined components
const ItemSeparator = () => <View style={styles.separator} />;
const EmptyComponent = () => <EmptyState message="No items" />;
const FooterComponent = () => <LoadingIndicator />;

Custom Virtualization

For complex layouts, implement custom virtualization:

const VirtualizedGrid: React.FC<Props> = ({ items, columns = 2 }) => {
  const [visibleRange, setVisibleRange] = useState({ start: 0, end: 10 });
  
  const handleScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
    const { contentOffset, layoutMeasurement } = event.nativeEvent;
    const itemHeight = ITEM_HEIGHT + ITEM_MARGIN;
    const rowsPerScreen = Math.ceil(layoutMeasurement.height / itemHeight);
    
    const startRow = Math.floor(contentOffset.y / itemHeight);
    const endRow = startRow + rowsPerScreen + 1; // Buffer
    
    setVisibleRange({
      start: startRow * columns,
      end: Math.min(endRow * columns, items.length)
    });
  }, [columns, items.length]);
 
  const visibleItems = useMemo(() => 
    items.slice(visibleRange.start, visibleRange.end),
    [items, visibleRange]
  );
 
  return (
    <ScrollView onScroll={handleScroll} scrollEventThrottle={16}>
      <View style={{ height: Math.ceil(items.length / columns) * (ITEM_HEIGHT + ITEM_MARGIN) }}>
        {visibleItems.map((item, index) => {
          const actualIndex = visibleRange.start + index;
          const row = Math.floor(actualIndex / columns);
          const col = actualIndex % columns;
          
          return (
            <View
              key={item.id}
              style={[
                styles.gridItem,
                {
                  position: 'absolute',
                  top: row * (ITEM_HEIGHT + ITEM_MARGIN),
                  left: col * (ITEM_WIDTH + ITEM_MARGIN),
                }
              ]}
            >
              <GridItem item={item} />
            </View>
          );
        })}
      </View>
    </ScrollView>
  );
};

Optimization Techniques

Image Optimization

Use Appropriate Formats

// ✅ Use FastImage for better caching
import FastImage from 'react-native-fast-image';
 
<FastImage
  style={styles.image}
  source={{
    uri: imageUrl,
    priority: FastImage.priority.normal,
  }}
  resizeMode={FastImage.resizeMode.cover}
/>

Implement Progressive Loading

const ProgressiveImage: React.FC<Props> = ({ source, thumbnail }) => {
  const [loaded, setLoaded] = useState(false);
  const opacity = useRef(new Animated.Value(0)).current;
  
  const onLoad = () => {
    Animated.timing(opacity, {
      toValue: 1,
      duration: 300,
      useNativeDriver: true,
    }).start();
    setLoaded(true);
  };
  
  return (
    <View>
      <FastImage
        source={{ uri: thumbnail }}
        style={styles.image}
        blurRadius={loaded ? 0 : 2}
      />
      <Animated.View style={[styles.imageOverlay, { opacity }]}>
        <FastImage
          source={{ uri: source }}
          style={styles.image}
          onLoad={onLoad}
        />
      </Animated.View>
    </View>
  );
};

Optimize Image Sizes

const getOptimizedImageUrl = (url: string, width: number) => {
  // Use image service that supports resizing
  return `${url}?w=${width}&q=75&fm=webp`;
};
 
const ResponsiveImage: React.FC<Props> = ({ source }) => {
  const { width } = useWindowDimensions();
  const imageUrl = useMemo(() => 
    getOptimizedImageUrl(source, width * PixelRatio.get()),
    [source, width]
  );
  
  return <FastImage source={{ uri: imageUrl }} />;
};

Animation Optimization

Use native driver and optimize animations:

// ❌ JS-driven animation (runs on JS thread)
Animated.timing(animatedValue, {
  toValue: 1,
  duration: 300,
  useNativeDriver: false, // Can't use native driver with layout props
}).start();
 
// ✅ Native-driven animation (runs on UI thread)
const AnimatedComponent: React.FC = () => {
  const translateY = useRef(new Animated.Value(0)).current;
  const opacity = useRef(new Animated.Value(0)).current;
 
  useEffect(() => {
    Animated.parallel([
      Animated.timing(translateY, {
        toValue: -20,
        duration: 300,
        useNativeDriver: true, // ✅ Runs on UI thread
      }),
      Animated.timing(opacity, {
        toValue: 1,
        duration: 300,
        useNativeDriver: true, // ✅ Runs on UI thread
      }),
    ]).start();
  }, []);
 
  return (
    <Animated.View
      style={{
        transform: [{ translateY }],
        opacity,
      }}
    >
      {/* Content */}
    </Animated.View>
  );
};

Reanimated for Complex Animations

Use Reanimated 2 for high-performance animations:

import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  withTiming,
  runOnJS,
} from 'react-native-reanimated';
 
const InteractiveComponent: React.FC = () => {
  const scale = useSharedValue(1);
  const rotation = useSharedValue(0);
 
  const animatedStyle = useAnimatedStyle(() => ({
    transform: [
      { scale: scale.value },
      { rotateZ: `${rotation.value}deg` },
    ],
  }));
 
  const handlePress = () => {
    scale.value = withSpring(1.2, {}, () => {
      scale.value = withSpring(1);
    });
    rotation.value = withTiming(360, { duration: 500 }, () => {
      rotation.value = 0;
    });
  };
 
  return (
    <Animated.View style={[styles.box, animatedStyle]}>
      <TouchableOpacity onPress={handlePress}>
        <Text>Tap me!</Text>
      </TouchableOpacity>
    </Animated.View>
  );
};

Common Pitfalls and Solutions

Common Issues and Solutions

  1. Anonymous Functions in Render

    // ❌ Creates new function every render
    <Button onPress={() => handlePress(item.id)} />
     
    // ✅ Stable reference
    const handleItemPress = useCallback((id: string) => {
      handlePress(id);
    }, [handlePress]);
  2. Inline Styles

    // ❌ Creates new object every render
    <View style={{ flex: 1, padding: 20 }} />
     
    // ✅ Use StyleSheet
    <View style={styles.container} />
  3. Array Index as Key

    // ❌ Causes re-render issues
    items.map((item, index) => <Item key={index} />)
     
    // ✅ Use stable unique key
    items.map((item) => <Item key={item.id} />)

Performance Monitoring

Performance Observer

Monitor component performance in development:

const usePerformanceMonitor = (componentName: string) => {
  const renderCount = useRef(0);
  const renderTime = useRef(performance.now());
  
  useEffect(() => {
    renderCount.current += 1;
    const now = performance.now();
    const timeSinceLastRender = now - renderTime.current;
    renderTime.current = now;
    
    if (__DEV__) {
      console.log(`[Performance] ${componentName}:`, {
        renderCount: renderCount.current,
        timeSinceLastRender: `${timeSinceLastRender.toFixed(2)}ms`,
      });
    }
  });
};
 
// Usage
const MyComponent: React.FC = () => {
  usePerformanceMonitor('MyComponent');
  // Component logic
};

Best Practices

Performance Best Practices

  1. Profile before optimizing - Use React DevTools Profiler
  2. Optimize the critical path - Focus on initial render
  3. Avoid premature optimization - Measure first
  4. Use production builds for performance testing
  5. Test on low-end devices - They reveal issues
  6. Monitor bundle size - Use Metro bundle analyzer

Performance Guidelines

  • (Do ✅) Use FlatList for long lists
  • (Do ✅) Implement getItemLayout when possible
  • (Do ✅) Use InteractionManager for post-interaction work
  • (Do ✅) Profile on real devices
  • (Don't ❌) Nest FlatLists or ScrollViews
  • (Don't ❌) Use array index as key in lists
  • (Consider 🤔) React.memo for expensive components
  • (Be Aware ❗) Of re-render cascades in large trees

Summary

Performance optimization in React Native requires:

  • Understanding the render cycle and bridge communication
  • Strategic memoization to prevent unnecessary work
  • Efficient list rendering with proper virtualization
  • Native-driven animations for smooth interactions
  • Continuous monitoring and profiling

Always measure performance impact before and after optimizations.

Next Steps