Kévin La Rosa

Senior Mobile Developer | iOS & React Native

React Native Performance Optimization

React Native Performance Optimization

Jun 1, 20257 min read

Building React Native applications for production taught me that performance isn't just about speed - it's about creating smooth experiences. Here are the optimization strategies that made the biggest difference in my projects.

Table of Contents

  1. Memory Leaks: The Silent App Killer
  2. FlatList Performance Optimization
  3. Image Optimization Strategies
  4. React Component Performance
  5. Navigation Performance
  6. Bundle Size & Startup Time
  7. Performance Testing & Monitoring
  8. Native Modules for Performance
  9. Real Device Testing

Memory Leaks: The Silent App Killer

One of the hardest bugs I've debugged was an app that crashed after 10 minutes of use. The culprit? Memory leaks from uncleaned event listeners and timers. Now I always implement proper cleanup patterns.

How to Fix React Native Memory Leaks

typescript
// Pattern I use for safe cleanup
const useSafeCleanup = () => {
  useEffect(() => {
    const listener = eventEmitter.addListener('update', handleUpdate);
    const timeout = setTimeout(loadData, 5000);

    return () => {
      listener.remove();
      clearTimeout(timeout);
    };
  }, []);
};

**Memory Leak Detection Tools:**

  • **Android**: LeakCanary automatically detects memory leaks
  • **iOS**: Instruments Memory Graph shows retain cycles
  • **Cross-platform**: Flipper with LeakCanary plugin

Making Lists Fly: FlatList Optimization

Every React Native developer knows the pain of janky scrolling. Through trial and error, I discovered that FlatList performance isn't just about one setting - it's about finding the right combination.

Essential FlatList Performance Props

The game-changers for me were:

  • **getItemLayout**: Skip measurement phase for fixed-height items (30% faster)
  • **windowSize**: Control memory usage vs smooth scrolling
  • **maxToRenderPerBatch**: Balance between responsiveness and performance
  • **removeClippedSubviews**: Essential for lists over 100 items
typescript
// My go-to FlatList setup for optimal performance
const PerformantList = () => {
  const renderItem = useCallback(({ item }) => (
    <ItemComponent data={item} />
  ), []);

  return (
    <FlatList
      data={listData}
      renderItem={renderItem}
      keyExtractor={item => item.id}
      getItemLayout={(_, index) => ({
        length: 80,
        offset: 80 * index,
        index,
      })}
      windowSize={10}
      maxToRenderPerBatch={5}
      initialNumToRender={10}
    />
  );
};

**Pro tip**: Shopify's FlashList implements view recycling similar to native RecyclerView, offering 10x better performance for large lists.

Image Optimization

Images can make or break your app's performance. The most impactful changes I've made:

WebP Format: 30-40% Size Reduction

Switching to WebP reduced image sizes dramatically compared to JPEG/PNG, with no visible quality loss. Most CDNs now support automatic WebP conversion.

Strategic Prefetching

For critical images (like hero banners or product photos), prefetching while users navigate dramatically improves perceived performance.

typescript
// Prefetch images before navigation
const prefetchImages = async (imageUrls: string[]) => {
  await Promise.all(imageUrls.map(url => Image.prefetch(url)));
};

// In navigation handler
const navigateToProduct = async (productId: string) => {
  const images = await getProductImages(productId);
  prefetchImages(images); // Start loading immediately
  navigation.navigate('ProductDetail', { productId });
};

**Recommended Libraries:**

  • react-native-fast-image: Better caching and performance
  • expo-image: Modern image component with built-in optimization
  • react-native-nitro-image: Built for performance from the ground up

Smart Component Updates

Understanding when and why components re-render transformed how I write React Native code. Not every component needs memoization, but knowing which ones do is crucial.

When to Use React.memo

Through experience, I've developed clear criteria for memoization. Always memoize components that perform expensive calculations or complex renders - the performance gain far outweighs the minimal memoization overhead. Visual components with animations or complex layouts benefit significantly from memoization.

List items deserve special attention - unmemoized list items cause every item to re-render when any part of the list changes. This creates exponential performance degradation as lists grow. Components receiving objects or arrays as props also need memoization since JavaScript creates new references on every render, triggering unnecessary updates.

typescript
// Strategic memoization with custom comparison
const DataCard = React.memo(({ info, onSelect }) => {
  return (
    <Pressable onPress={onSelect}>
      <ExpensiveVisualization data={info} />
    </Pressable>
  );
}, (prev, next) => {
  // Only re-render if data actually changed
  return prev.info.id === next.info.id;
});

The Future: React Compiler

React Compiler (formerly React Forget) promises to automatically optimize components without manual memoization:

  • No more useMemo, useCallback, or React.memo boilerplate
  • Compiler automatically detects what needs optimization
  • Prevents common mistakes like missing dependencies
  • Could eliminate entire categories of performance issues

Until then, manual optimization remains essential, but the future looks much simpler.

Switching from JavaScript navigation to native stack navigation was like night and day. The native implementation automatically manages memory by unmounting invisible screens.

Enable React Native Screens

typescript
// Enable native optimizations
import { enableFreeze } from 'react-native-screens';
enableFreeze(true);

// Native stack for better memory management
const Stack = createNativeStackNavigator();

**Performance benefits:**

  • Unmounts invisible screens (saves 50-70% memory)
  • Prevents background re-renders
  • Native transitions are smoother

App Launch Speed & Bundle Size

Users judge apps in the first few seconds. Here's how I achieved 70% faster startup times:

1. Enable Hermes Engine

Hermes compiles JavaScript ahead of time:

  • 30% smaller bundle size
  • 50% faster startup
  • Better memory usage

2. Analyze Bundle Size with expo-atlas

bash
# Generate bundle analysis
npx expo-atlas build/output/index.html

Common savings I've found:

  • **Moment.js → date-fns**: saved 200KB
  • **Lodash full import → cherry-pick**: saved 150KB
  • **Duplicate dependencies → yarn resolutions**: saved 100KB+

3. Implement Code Splitting

typescript
// Lazy loading non-critical screens
const ProfileScreen = React.lazy(() => import('./screens/Profile'));
const SettingsScreen = React.lazy(() => import('./screens/Settings'));

const App = () => (
  <Suspense fallback={<LoadingView />}>
    <Stack.Navigator>
      <Stack.Screen name="Home" component={HomeScreen} />
      <Stack.Screen name="Profile" component={ProfileScreen} />
      <Stack.Screen name="Settings" component={SettingsScreen} />
    </Stack.Navigator>
  </Suspense>
);

Performance Testing & Monitoring

Automated Performance Testing with Reassure

Reassure measures component render performance and catches regressions in CI/CD:

typescript
// Performance test with Reassure
import { measurePerformance } from 'reassure';

test('ProductList performance', async () => {
  const scenario = async () => {
    render(<ProductList items={mockProducts} />);
  };

  await measurePerformance(scenario, {
    runs: 10,
    warmupRuns: 3,
  });
});

E2E Performance Testing with Flashlight

Flashlight measures performance during actual user flows:

bash
# Run Flashlight with your E2E tests
npx flashlight test --bundleId com.yourapp \
  --testCommand "yarn e2e:test" \
  --duration 10000

Generates reports showing:

  • FPS timeline
  • CPU usage graphs
  • Memory consumption
  • JS thread activity

Going Native When Needed

Sometimes JavaScript isn't fast enough. Don't be afraid to use native code:

Expo Modules

Perfect for platform-specific UI components using Swift/Kotlin.

Nitro Modules

For performance-critical code using C++ and JSI, enabling synchronous native calls.

Testing on Real Devices

The biggest eye-opener was testing on old, low-end devices. A 2017 Android phone with 2GB RAM reveals issues that modern phones hide.

Essential Performance Tools

For iOS development, I rely on Instruments for CPU profiling and memory debugging, while Core Animation helps track FPS and rendering issues.

On Android, Systrace provides excellent thread activity visualization, and Android Studio Profiler covers memory, CPU, and network analysis comprehensively.

Performance Debugging Helper

typescript
// Simple performance marker for debugging
const perfMark = (label: string) => {
  if (__DEV__) {
    performance.mark(label);
    console.log(`[PERF] ${label}: ${performance.now()}ms`);
  }
};

Key Performance Metrics

**Critical thresholds:**

  • FPS < 60 = visible jank
  • Startup > 3 seconds = user abandonment
  • Memory growth = potential leaks
  • JS thread blocked > 16ms = frame drop

Conclusion: Real-World Performance

The most valuable lesson? Test early and often on the worst devices you can find. A 2GB RAM Android phone from 2017 will teach you more about performance than any profiler.

Remember: your users aren't using your development machine. They're on crowded subways with spotty connections, using phones with dozens of apps fighting for memory. Build for them, not for your test environment.

Have questions about React Native performance? Reach out - I'm always happy to discuss optimization strategies!