Home/Blog/Mobile App Performance Optimization: Advanced Tech...
React Native

Mobile App Performance Optimization: Advanced Techniques for React Native

Deep dive into performance optimization for React Native apps. Learn profiling, memory management, rendering optimization, and advanced techniques to build lightning-fast mobile apps.

Sani Mridha

Sani Mridha

Senior Mobile Developer

📅 2023-12-28⏱️ 20 min read

Mobile App Performance Optimization: Advanced Techniques for React Native

Performance can make or break your mobile app. Let's explore advanced techniques to build lightning-fast React Native applications.

Why Performance Matters

The Numbers

  • 53% of users abandon apps that take more than 3 seconds to load
  • 70% of users cite slow performance as the reason for deletion
  • 1 second delay can reduce conversions by 7%
  • User Experience Impact

  • Fast apps = Happy users = Better ratings = More downloads
  • Slow apps = Frustration = Uninstalls = Poor reviews
  • Performance Profiling

    React DevTools Profiler

    import { Profiler } from 'react';
    
    function onRenderCallback(
      id: string,
      phase: "mount" | "update",
      actualDuration: number,
      baseDuration: number,
      startTime: number,
      commitTime: number
    ) {
      console.log(`${id} took ${actualDuration}ms to render`);
    }
    
    <Profiler id="MyComponent" onRender={onRenderCallback}>
      <MyComponent />
    </Profiler>

    Flipper Performance Plugin

    # Install Flipper
    brew install --cask flipper
    
    # Enable performance monitoring
    # In your app, the React Native Flipper plugin is automatically enabled

    Custom Performance Monitoring

    class PerformanceMonitor {
      private metrics: Map<string, number[]> = new Map();
    
      startMeasure(label: string) {
        performance.mark(`${label}-start`);
      }
    
      endMeasure(label: string) {
        performance.mark(`${label}-end`);
        performance.measure(label, `${label}-start`, `${label}-end`);
        
        const measure = performance.getEntriesByName(label)[0];
        const duration = measure.duration;
        
        if (!this.metrics.has(label)) {
          this.metrics.set(label, []);
        }
        this.metrics.get(label)!.push(duration);
        
        // Log if slow
        if (duration > 16.67) { // 60 FPS threshold
          console.warn(`Slow operation: ${label} took ${duration.toFixed(2)}ms`);
        }
      }
    
      getStats(label: string) {
        const durations = this.metrics.get(label) || [];
        return {
          avg: durations.reduce((a, b) => a + b, 0) / durations.length,
          min: Math.min(...durations),
          max: Math.max(...durations),
          count: durations.length
        };
      }
    }
    
    const monitor = new PerformanceMonitor();
    
    // Usage
    monitor.startMeasure('data-fetch');
    await fetchData();
    monitor.endMeasure('data-fetch');

    Rendering Optimization

    1. Memoization

    import { memo, useMemo, useCallback } from 'react';
    
    // Memoize components
    const ExpensiveComponent = memo(({ data, onPress }) => {
      return (
        <View>
          {data.map(item => <Item key={item.id} {...item} />)}
        </View>
      );
    }, (prevProps, nextProps) => {
      // Custom comparison
      return prevProps.data.length === nextProps.data.length;
    });
    
    // Memoize computed values
    function MyComponent({ items }) {
      const sortedItems = useMemo(() => {
        return items.sort((a, b) => a.name.localeCompare(b.name));
      }, [items]);
      
      const handlePress = useCallback((id: string) => {
        // Handle press
      }, []);
      
      return <List items={sortedItems} onPress={handlePress} />;
    }

    2. FlatList Optimization

    import { FlatList } from 'react-native';
    
    <FlatList
      data={items}
      renderItem={renderItem}
      keyExtractor={(item) => item.id}
      
      // Performance optimizations
      removeClippedSubviews={true}
      maxToRenderPerBatch={10}
      updateCellsBatchingPeriod={50}
      initialNumToRender={10}
      windowSize={5}
      
      // Use getItemLayout for fixed-height items
      getItemLayout={(data, index) => ({
        length: ITEM_HEIGHT,
        offset: ITEM_HEIGHT * index,
        index,
      })}
      
      // Optimize re-renders
      extraData={selectedId}
    />

    3. Image Optimization

    import FastImage from 'react-native-fast-image';
    
    // Use FastImage instead of Image
    <FastImage
      source={{
        uri: imageUrl,
        priority: FastImage.priority.normal,
        cache: FastImage.cacheControl.immutable,
      }}
      resizeMode={FastImage.resizeMode.cover}
      style={styles.image}
    />
    
    // Preload images
    FastImage.preload([
      { uri: 'https://example.com/image1.jpg' },
      { uri: 'https://example.com/image2.jpg' },
    ]);
    
    // Progressive image loading
    const [imageLoaded, setImageLoaded] = useState(false);
    
    <View>
      {!imageLoaded && <Skeleton />}
      <FastImage
        source={{ uri: imageUrl }}
        onLoadEnd={() => setImageLoaded(true)}
        style={[styles.image, { opacity: imageLoaded ? 1 : 0 }]}
      />
    </View>

    Memory Management

    1. Cleanup Side Effects

    useEffect(() => {
      const subscription = eventEmitter.addListener('event', handler);
      const interval = setInterval(updateData, 1000);
      
      return () => {
        // Cleanup
        subscription.remove();
        clearInterval(interval);
      };
    }, []);

    2. Avoid Memory Leaks

    // Bad: Creates new function on every render
    <Button onPress={() => handlePress(item.id)} />
    
    // Good: Use useCallback
    const handlePress = useCallback((id: string) => {
      // Handle press
    }, []);
    
    <Button onPress={() => handlePress(item.id)} />
    
    // Better: Pass stable reference
    const handlePressFactory = useCallback((id: string) => () => {
      // Handle press
    }, []);
    
    <Button onPress={handlePressFactory(item.id)} />

    3. Large Lists Memory Management

    import { RecyclerListView, DataProvider, LayoutProvider } from 'recyclerlistview';
    
    // RecyclerListView recycles views for better memory usage
    const dataProvider = new DataProvider((r1, r2) => r1.id !== r2.id);
    const layoutProvider = new LayoutProvider(
      (index) => 'ITEM',
      (type, dim) => {
        dim.width = width;
        dim.height = 100;
      }
    );
    
    <RecyclerListView
      dataProvider={dataProvider.cloneWithRows(items)}
      layoutProvider={layoutProvider}
      rowRenderer={renderRow}
    />

    Network Optimization

    1. Request Batching

    class RequestBatcher {
      private queue: Array<{
        endpoint: string;
        resolve: (data: any) => void;
        reject: (error: any) => void;
      }> = [];
      private timeout: NodeJS.Timeout | null = null;
    
      request(endpoint: string): Promise<any> {
        return new Promise((resolve, reject) => {
          this.queue.push({ endpoint, resolve, reject });
          
          if (this.timeout) {
            clearTimeout(this.timeout);
          }
          
          this.timeout = setTimeout(() => this.flush(), 50);
        });
      }
    
      private async flush() {
        const batch = this.queue.splice(0);
        
        try {
          const response = await fetch('/api/batch', {
            method: 'POST',
            body: JSON.stringify({
              requests: batch.map(r => r.endpoint)
            })
          });
          
          const results = await response.json();
          
          batch.forEach((item, index) => {
            item.resolve(results[index]);
          });
        } catch (error) {
          batch.forEach(item => item.reject(error));
        }
      }
    }
    
    const batcher = new RequestBatcher();
    
    // Usage
    const data1 = await batcher.request('/api/user/1');
    const data2 = await batcher.request('/api/user/2');
    // Both requests are batched into a single network call

    2. Caching Strategy

    import AsyncStorage from '@react-native-async-storage/async-storage';
    
    class CacheManager {
      private memoryCache = new Map<string, { data: any; timestamp: number }>();
      private readonly TTL = 5 * 60 * 1000; // 5 minutes
    
      async get(key: string): Promise<any | null> {
        // Check memory cache first
        const memCached = this.memoryCache.get(key);
        if (memCached && Date.now() - memCached.timestamp < this.TTL) {
          return memCached.data;
        }
    
        // Check persistent cache
        try {
          const cached = await AsyncStorage.getItem(key);
          if (cached) {
            const { data, timestamp } = JSON.parse(cached);
            if (Date.now() - timestamp < this.TTL) {
              this.memoryCache.set(key, { data, timestamp });
              return data;
            }
          }
        } catch (error) {
          console.error('Cache read error:', error);
        }
    
        return null;
      }
    
      async set(key: string, data: any): Promise<void> {
        const timestamp = Date.now();
        
        // Set memory cache
        this.memoryCache.set(key, { data, timestamp });
        
        // Set persistent cache
        try {
          await AsyncStorage.setItem(key, JSON.stringify({ data, timestamp }));
        } catch (error) {
          console.error('Cache write error:', error);
        }
      }
    
      clear() {
        this.memoryCache.clear();
        AsyncStorage.clear();
      }
    }
    
    const cache = new CacheManager();
    
    // Usage
    async function fetchUserData(userId: string) {
      const cacheKey = `user_${userId}`;
      
      // Try cache first
      const cached = await cache.get(cacheKey);
      if (cached) return cached;
      
      // Fetch from network
      const data = await fetch(`/api/users/${userId}`).then(r => r.json());
      
      // Cache the result
      await cache.set(cacheKey, data);
      
      return data;
    }

    3. Optimistic Updates

    function useLikePost() {
      const [isLiked, setIsLiked] = useState(false);
      const [likeCount, setLikeCount] = useState(0);
    
      const toggleLike = async (postId: string) => {
        // Optimistic update
        const previousLiked = isLiked;
        const previousCount = likeCount;
        
        setIsLiked(!isLiked);
        setLikeCount(prev => prev + (isLiked ? -1 : 1));
    
        try {
          await fetch(`/api/posts/${postId}/like`, {
            method: isLiked ? 'DELETE' : 'POST'
          });
        } catch (error) {
          // Rollback on error
          setIsLiked(previousLiked);
          setLikeCount(previousCount);
          showError('Failed to update like');
        }
      };
    
      return { isLiked, likeCount, toggleLike };
    }

    Animation Performance

    1. Use Native Driver

    import { Animated } from 'react-native';
    
    const fadeAnim = useRef(new Animated.Value(0)).current;
    
    // Always use native driver when possible
    Animated.timing(fadeAnim, {
      toValue: 1,
      duration: 300,
      useNativeDriver: true, // ✅ Runs on native thread
    }).start();
    
    // For transforms and opacity only
    Animated.parallel([
      Animated.timing(fadeAnim, {
        toValue: 1,
        useNativeDriver: true,
      }),
      Animated.timing(translateY, {
        toValue: 0,
        useNativeDriver: true,
      }),
    ]).start();

    2. Reanimated for Complex Animations

    import Animated, {
      useSharedValue,
      useAnimatedStyle,
      withSpring,
      withTiming,
    } from 'react-native-reanimated';
    
    function AnimatedComponent() {
      const offset = useSharedValue(0);
    
      const animatedStyles = useAnimatedStyle(() => ({
        transform: [{ translateX: offset.value }],
      }));
    
      const handlePress = () => {
        offset.value = withSpring(100);
      };
    
      return (
        <Animated.View style={animatedStyles}>
          <TouchableOpacity onPress={handlePress}>
            <Text>Animate</Text>
          </TouchableOpacity>
        </Animated.View>
      );
    }

    3. Gesture Handler

    import { GestureDetector, Gesture } from 'react-native-gesture-handler';
    import Animated, { useSharedValue, useAnimatedStyle } from 'react-native-reanimated';
    
    function DraggableBox() {
      const translateX = useSharedValue(0);
      const translateY = useSharedValue(0);
    
      const pan = Gesture.Pan()
        .onChange((event) => {
          translateX.value += event.changeX;
          translateY.value += event.changeY;
        });
    
      const animatedStyle = useAnimatedStyle(() => ({
        transform: [
          { translateX: translateX.value },
          { translateY: translateY.value },
        ],
      }));
    
      return (
        <GestureDetector gesture={pan}>
          <Animated.View style={[styles.box, animatedStyle]} />
        </GestureDetector>
      );
    }

    Bundle Size Optimization

    1. Code Splitting

    // Lazy load screens
    const ProfileScreen = lazy(() => import('./screens/ProfileScreen'));
    const SettingsScreen = lazy(() => import('./screens/SettingsScreen'));
    
    // Use Suspense
    <Suspense fallback={<LoadingScreen />}>
      <ProfileScreen />
    </Suspense>

    2. Remove Unused Dependencies

    # Analyze bundle
    npx react-native-bundle-visualizer
    
    # Remove unused dependencies
    npm uninstall unused-package
    
    # Use smaller alternatives
    # Instead of moment (67KB), use date-fns (13KB)
    npm install date-fns

    3. Hermes Engine

    // android/app/build.gradle
    project.ext.react = [
        enableHermes: true  // ✅ Enable Hermes
    ]
    
    // Benefits:
    // - Faster app startup
    // - Reduced memory usage
    // - Smaller app size

    Startup Performance

    1. Reduce Initial Bundle

    // Split into multiple bundles
    // metro.config.js
    module.exports = {
      serializer: {
        createModuleIdFactory: () => {
          return (path) => {
            // Create stable module IDs for better caching
            return hash(path);
          };
        },
      },
    };

    2. Lazy Initialization

    // Don't initialize everything on startup
    class Analytics {
      private static instance: Analytics | null = null;
      
      static getInstance() {
        if (!this.instance) {
          this.instance = new Analytics();
        }
        return this.instance;
      }
      
      private constructor() {
        // Heavy initialization
      }
    }
    
    // Initialize only when needed
    const analytics = Analytics.getInstance();

    3. Splash Screen Optimization

    import SplashScreen from 'react-native-splash-screen';
    
    function App() {
      useEffect(() => {
        // Hide splash screen after app is ready
        const prepare = async () => {
          await loadCriticalData();
          SplashScreen.hide();
        };
        
        prepare();
      }, []);
      
      return <AppContent />;
    }

    Monitoring in Production

    1. Performance Metrics

    import { PerformanceObserver } from 'react-native-performance';
    
    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        // Send to analytics
        analytics.track('performance', {
          name: entry.name,
          duration: entry.duration,
          startTime: entry.startTime,
        });
      });
    });
    
    observer.observe({ entryTypes: ['measure', 'navigation'] });

    2. Crash Reporting

    import crashlytics from '@react-native-firebase/crashlytics';
    
    // Log errors
    crashlytics().log('User performed action');
    
    // Record errors
    try {
      // risky operation
    } catch (error) {
      crashlytics().recordError(error);
    }
    
    // Set user attributes
    crashlytics().setUserId(userId);
    crashlytics().setAttribute('role', 'premium');

    3. Custom Metrics

    import perf from '@react-native-firebase/perf';
    
    async function fetchUserData() {
      const trace = await perf().startTrace('user_data_fetch');
      
      try {
        const data = await fetch('/api/user');
        trace.putAttribute('cache_hit', 'false');
        return data;
      } finally {
        await trace.stop();
      }
    }

    Conclusion

    Performance optimization is an ongoing process:

    1. Measure first - Profile before optimizing

    2. Focus on impact - Optimize the biggest bottlenecks

    3. Test on real devices - Emulators don't show real performance

    4. Monitor production - Track metrics in the wild

    5. Iterate continuously - Performance degrades over time

    Remember: Premature optimization is the root of all evil. Optimize when you have data showing it's needed!

    ---

    *Need help optimizing your React Native app? Let's chat!*

    Tags

    #React Native#Performance#Mobile Development#Optimization

    Share this article

    Let's Work Together

    Need help with your mobile app or have a project in mind?

    Sani Mridha - Senior React Native Developer | iOS & Android Expert