react-native-sop-docs/07-performance-optimization/optimization.md

11 KiB

Performance Optimization

Hermes Setup

Enable Hermes

Android (android/app/build.gradle):

project.ext.react = [
    enableHermes: true
]

iOS (Podfile):

use_react_native!(
  :path => config[:reactNativePath],
  :hermes_enabled => true
)

Hermes Benefits

  • Faster app startup time
  • Reduced memory usage
  • Smaller bundle size
  • Better performance on lower-end devices

List Optimization

Optimized FlatList

// src/components/OptimizedList/OptimizedList.tsx
import React, {useCallback, useMemo} from 'react';
import {FlatList, ListRenderItem, ViewToken} from 'react-native';

interface OptimizedListProps<T> {
  data: T[];
  renderItem: ListRenderItem<T>;
  keyExtractor: (item: T, index: number) => string;
  itemHeight?: number;
  onEndReached?: () => void;
  onViewableItemsChanged?: (info: {viewableItems: ViewToken[]}) => void;
}

export const OptimizedList = <T,>({
  data,
  renderItem,
  keyExtractor,
  itemHeight = 100,
  onEndReached,
  onViewableItemsChanged,
}: OptimizedListProps<T>) => {
  const memoizedRenderItem = useCallback(renderItem, []);
  const memoizedKeyExtractor = useCallback(keyExtractor, []);

  const getItemLayout = useMemo(
    () =>
      itemHeight
        ? (data: any, index: number) => ({
            length: itemHeight,
            offset: itemHeight * index,
            index,
          })
        : undefined,
    [itemHeight]
  );

  const viewabilityConfig = useMemo(
    () => ({
      itemVisiblePercentThreshold: 50,
      minimumViewTime: 300,
    }),
    []
  );

  return (
    <FlatList
      data={data}
      renderItem={memoizedRenderItem}
      keyExtractor={memoizedKeyExtractor}
      getItemLayout={getItemLayout}
      removeClippedSubviews={true}
      maxToRenderPerBatch={10}
      windowSize={10}
      initialNumToRender={10}
      updateCellsBatchingPeriod={50}
      onEndReached={onEndReached}
      onEndReachedThreshold={0.5}
      onViewableItemsChanged={onViewableItemsChanged}
      viewabilityConfig={viewabilityConfig}
    />
  );
};

Virtualized List for Large Datasets

// src/components/VirtualizedList/VirtualizedList.tsx
import React from 'react';
import {VirtualizedList, ListRenderItem} from 'react-native';

interface VirtualizedListProps<T> {
  data: T[];
  renderItem: ListRenderItem<T>;
  keyExtractor: (item: T, index: number) => string;
  itemHeight: number;
}

export const CustomVirtualizedList = <T,>({
  data,
  renderItem,
  keyExtractor,
  itemHeight,
}: VirtualizedListProps<T>) => {
  const getItem = (data: T[], index: number) => data[index];
  const getItemCount = (data: T[]) => data.length;

  return (
    <VirtualizedList
      data={data}
      initialNumToRender={4}
      renderItem={renderItem}
      keyExtractor={keyExtractor}
      getItemCount={getItemCount}
      getItem={getItem}
      getItemLayout={(data, index) => ({
        length: itemHeight,
        offset: itemHeight * index,
        index,
      })}
    />
  );
};

Image Optimization

Lazy Loading Images

// src/components/LazyImage/LazyImage.tsx
import React, {useState, useRef} from 'react';
import {View, Image, Animated, StyleSheet} from 'react-native';
import FastImage from 'react-native-fast-image';

interface LazyImageProps {
  source: {uri: string};
  style?: any;
  placeholder?: React.ReactNode;
}

export const LazyImage: React.FC<LazyImageProps> = ({
  source,
  style,
  placeholder,
}) => {
  const [loaded, setLoaded] = useState(false);
  const opacity = useRef(new Animated.Value(0)).current;

  const onLoad = () => {
    setLoaded(true);
    Animated.timing(opacity, {
      toValue: 1,
      duration: 300,
      useNativeDriver: true,
    }).start();
  };

  return (
    <View style={style}>
      {!loaded && placeholder}
      <Animated.View style={[StyleSheet.absoluteFill, {opacity}]}>
        <FastImage
          source={source}
          style={StyleSheet.absoluteFill}
          onLoad={onLoad}
          resizeMode={FastImage.resizeMode.cover}
        />
      </Animated.View>
    </View>
  );
};

Memory Management

Component Memoization

// src/components/MemoizedComponent/MemoizedComponent.tsx
import React, {memo, useMemo, useCallback} from 'react';
import {View, Text, TouchableOpacity} from 'react-native';

interface ItemProps {
  id: string;
  title: string;
  description: string;
  onPress: (id: string) => void;
}

const ItemComponent: React.FC<ItemProps> = memo(({
  id,
  title,
  description,
  onPress,
}) => {
  const handlePress = useCallback(() => {
    onPress(id);
  }, [id, onPress]);

  const formattedDescription = useMemo(() => {
    return description.length > 100 
      ? `${description.substring(0, 100)}...` 
      : description;
  }, [description]);

  return (
    <TouchableOpacity onPress={handlePress}>
      <View>
        <Text>{title}</Text>
        <Text>{formattedDescription}</Text>
      </View>
    </TouchableOpacity>
  );
});

export default ItemComponent;

Custom Hooks Optimization

// src/hooks/useOptimizedData.ts
import {useMemo, useCallback} from 'react';
import {useAppSelector} from '@redux/hooks';

export const useOptimizedData = (filter?: string) => {
  const rawData = useAppSelector(state => state.data.items);

  const filteredData = useMemo(() => {
    if (!filter) return rawData;
    return rawData.filter(item => 
      item.title.toLowerCase().includes(filter.toLowerCase())
    );
  }, [rawData, filter]);

  const processItem = useCallback((item: any) => {
    return {
      ...item,
      displayTitle: item.title.toUpperCase(),
      isNew: Date.now() - item.createdAt < 86400000, // 24 hours
    };
  }, []);

  const processedData = useMemo(() => 
    filteredData.map(processItem), 
    [filteredData, processItem]
  );

  return processedData;
};

Bundle Optimization

Code Splitting

// src/utils/lazyImports.ts
import {lazy} from 'react';

// Lazy load heavy screens
export const ProfileScreen = lazy(() => import('@screens/Profile/ProfileScreen'));
export const SettingsScreen = lazy(() => import('@screens/Settings/SettingsScreen'));
export const ReportsScreen = lazy(() => import('@screens/Reports/ReportsScreen'));

// Lazy load heavy components
export const ChartComponent = lazy(() => import('@components/Chart/ChartComponent'));
export const MapComponent = lazy(() => import('@components/Map/MapComponent'));

Dynamic Imports

// src/utils/dynamicImports.ts
export const loadChartLibrary = async () => {
  const {Chart} = await import('react-native-chart-kit');
  return Chart;
};

export const loadMapLibrary = async () => {
  const MapView = await import('react-native-maps');
  return MapView.default;
};

// Usage in component
const MyComponent = () => {
  const [ChartComponent, setChartComponent] = useState(null);

  useEffect(() => {
    loadChartLibrary().then(setChartComponent);
  }, []);

  return ChartComponent ? <ChartComponent /> : <LoadingSpinner />;
};

Animation Optimization

Native Driver Usage

// src/animations/optimizedAnimations.ts
import {Animated, Easing} from 'react-native';

export const createOptimizedAnimation = (
  animatedValue: Animated.Value,
  toValue: number,
  duration: number = 300
) => {
  return Animated.timing(animatedValue, {
    toValue,
    duration,
    easing: Easing.out(Easing.quad),
    useNativeDriver: true, // Use native driver for better performance
  });
};

export const createSpringAnimation = (
  animatedValue: Animated.Value,
  toValue: number
) => {
  return Animated.spring(animatedValue, {
    toValue,
    tension: 100,
    friction: 8,
    useNativeDriver: true,
  });
};

Network Optimization

Request Batching

// src/utils/requestBatcher.ts
class RequestBatcher {
  private queue: Array<{
    url: string;
    options: RequestInit;
    resolve: (value: any) => void;
    reject: (reason: any) => void;
  }> = [];
  private batchTimeout: NodeJS.Timeout | null = null;

  public request(url: string, options: RequestInit = {}): Promise<any> {
    return new Promise((resolve, reject) => {
      this.queue.push({url, options, resolve, reject});
      
      if (this.batchTimeout) {
        clearTimeout(this.batchTimeout);
      }
      
      this.batchTimeout = setTimeout(() => {
        this.processBatch();
      }, 50); // Batch requests within 50ms
    });
  }

  private async processBatch() {
    const batch = [...this.queue];
    this.queue = [];
    this.batchTimeout = null;

    // Group similar requests
    const grouped = batch.reduce((acc, req) => {
      const key = `${req.options.method || 'GET'}-${req.url.split('?')[0]}`;
      if (!acc[key]) acc[key] = [];
      acc[key].push(req);
      return acc;
    }, {} as Record<string, typeof batch>);

    // Process each group
    for (const requests of Object.values(grouped)) {
      try {
        const results = await Promise.all(
          requests.map(req => fetch(req.url, req.options))
        );
        
        results.forEach((result, index) => {
          requests[index].resolve(result);
        });
      } catch (error) {
        requests.forEach(req => req.reject(error));
      }
    }
  }
}

export const requestBatcher = new RequestBatcher();

Performance Monitoring

Performance Metrics

// src/utils/performanceMonitor.ts
class PerformanceMonitor {
  private metrics: Map<string, number> = new Map();

  startTiming(label: string) {
    this.metrics.set(label, Date.now());
  }

  endTiming(label: string): number {
    const startTime = this.metrics.get(label);
    if (!startTime) {
      console.warn(`No start time found for ${label}`);
      return 0;
    }

    const duration = Date.now() - startTime;
    this.metrics.delete(label);
    
    console.log(`${label}: ${duration}ms`);
    return duration;
  }

  measureAsync<T>(label: string, fn: () => Promise<T>): Promise<T> {
    this.startTiming(label);
    return fn().finally(() => {
      this.endTiming(label);
    });
  }

  measureSync<T>(label: string, fn: () => T): T {
    this.startTiming(label);
    try {
      return fn();
    } finally {
      this.endTiming(label);
    }
  }
}

export const performanceMonitor = new PerformanceMonitor();

// Usage
performanceMonitor.measureAsync('API_CALL', () => 
  fetch('/api/data').then(res => res.json())
);

Memory Usage Tracking

// src/utils/memoryTracker.ts
export const trackMemoryUsage = () => {
  if (__DEV__) {
    const memoryInfo = (performance as any).memory;
    if (memoryInfo) {
      console.log('Memory Usage:', {
        used: Math.round(memoryInfo.usedJSHeapSize / 1048576) + ' MB',
        total: Math.round(memoryInfo.totalJSHeapSize / 1048576) + ' MB',
        limit: Math.round(memoryInfo.jsHeapSizeLimit / 1048576) + ' MB',
      });
    }
  }
};

// Track memory usage periodically
setInterval(trackMemoryUsage, 30000); // Every 30 seconds in development