DEV Community

Ajmal Hasan
Ajmal Hasan

Posted on β€’ Edited on

1 1

Optimizing React Native Performance: A Developer's Guide

As a React Native developer with years of experience, I've compiled my most valuable performance optimization techniques into this comprehensive guide. Save it for later and evaluate your projects against these battle-tested practices!

This e-book is best for detailed info. Check out Callstack E-Book

Code Optimization

1. Don't rely on useMemo & useCallback too much

Design your components to minimize memoization needs instead of defaulting to these hooks.

// Instead of excessive memoization:
const handlePress = useCallback(() => {
  navigateToProfile(user.id);
}, [user.id]);

// Consider if the simple approach works for your case:
const handlePress = () => navigateToProfile(user.id);
Enter fullscreen mode Exit fullscreen mode

Memoization itself has a performance cost. For most components, the overhead of recreating functions is negligible compared to the cost of tracking dependencies.

2. Don't rely on custom hooks too much

Custom hooks improve readability, not performance. Each instance uses separate resources.

// Each component using this creates separate instances
const useCounter = () => {
  const [count, setCount] = useState(0);
  return {
    count,
    increment: () => setCount(prev => prev + 1)
  };
};

// Better for widely-used functionality:
const globalCounter = {
  count: 0,
  listeners: new Set(),
  increment() {
    this.count++;
    this.listeners.forEach(listener => listener(this.count));
  },
  subscribe(listener) {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }
};
Enter fullscreen mode Exit fullscreen mode

3. Keep files small (~200 lines, including stylesheet)

Smaller files improve readability, maintainability, and compilation speed.

// ProfileScreen.js (~200 lines including styles)
import ProfileHeader from './ProfileHeader';
import ProfileContent from './ProfileContent';

const ProfileScreen = () => (
  <View style={styles.container}>
    <ProfileHeader />
    <ProfileContent />
  </View>
);

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff'
  }
});
Enter fullscreen mode Exit fullscreen mode

4. Follow SOLID (Single Responsibility)

Components should get data directly instead of through prop chains.

// Instead of prop drilling:
<App>
  <MainScreen>
    <Dashboard>
      <UserInfo userData={userData} />
    </Dashboard>
  </MainScreen>
</App>

// Better approach with direct access:
const UserInfo = () => {
  const { userData } = useUserContext(); // Or Redux/MobX/etc.
  return <Text>{userData.name}</Text>;
};
Enter fullscreen mode Exit fullscreen mode

5. Use useRef instead of useState if it doesn't reflect on the UI

For values that don't trigger re-renders, useRef is more efficient.

// Bad - causes re-renders when scrollPosition changes
const [scrollPosition, setScrollPosition] = useState(0);
const handleScroll = (event) => {
  setScrollPosition(event.nativeEvent.contentOffset.y);
};

// Good - no re-renders until needed
const scrollPositionRef = useRef(0);
const handleScroll = (event) => {
  scrollPositionRef.current = event.nativeEvent.contentOffset.y;
  if (scrollPositionRef.current > threshold) {
    // Only update state when needed
    setShowHeader(false);
  }
};
Enter fullscreen mode Exit fullscreen mode

6. Avoid inline styles, use StyleSheet

StyleSheet provides optimization and better performance.

// Bad
<View style={{ padding: 10, margin: 15, backgroundColor: '#fff' }}>
  <Text style={{ color: 'black', fontSize: 16 }}>Hello World</Text>
</View>

// Good
<View style={styles.container}>
  <Text style={styles.text}>Hello World</Text>
</View>

const styles = StyleSheet.create({
  container: {
    padding: 10,
    margin: 15,
    backgroundColor: '#fff'
  },
  text: {
    color: 'black',
    fontSize: 16
  }
});
Enter fullscreen mode Exit fullscreen mode

Rendering Optimization

7. Avoid calculations inside JSX

Process data before rendering for cleaner, more efficient code.

// Bad:
<Text>Output: {calculateSomething()}</Text>

// Good (value from store or pre-calculated):
<Text>Output: {preCalculatedValue}</Text>
Enter fullscreen mode Exit fullscreen mode
// Example implementation:
const ProductList = ({ products }) => {
  // Calculate once before rendering
  const totalPrice = useMemo(() => 
    products.reduce((sum, p) => sum + p.price, 0), 
    [products]
  );

  return (
    <View>
      <Text>Total: ${totalPrice}</Text>
      {products.map(product => (
        <ProductItem key={product.id} product={product} />
      ))}
    </View>
  );
};
Enter fullscreen mode Exit fullscreen mode

8. Lazy load Bottom/Top tabs to render only when needed

Load tab content only when the user visits that tab.

import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

const Tab = createBottomTabNavigator();

export default function AppTabs() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="Home" component={HomeScreen} />
      <Tab.Screen 
        name="Profile" 
        component={ProfileScreen}
        lazy={true} 
        lazyPlaceholder={() => <Loading />}
      />
      <Tab.Screen 
        name="Settings" 
        component={SettingsScreen}
        lazy={true}
      />
    </Tab.Navigator>
  );
}
Enter fullscreen mode Exit fullscreen mode

9. Don't render off-screen components in scroll views

Use a scroll handler to conditionally render components as they enter the viewport.

const LongScrollView = () => {
  const [visibleRange, setVisibleRange] = useState({ start: 0, end: 5 });

  const handleScroll = (event) => {
    const y = event.nativeEvent.contentOffset.y;
    const screenHeight = Dimensions.get('window').height;

    // Approximate which items are visible based on scroll position
    const startIndex = Math.max(0, Math.floor(y / ITEM_HEIGHT));
    const endIndex = Math.min(
      items.length - 1,
      Math.ceil((y + screenHeight) / ITEM_HEIGHT)
    );

    setVisibleRange({ start: startIndex, end: endIndex });
  };

  return (
    <ScrollView 
      onScroll={handleScroll}
      scrollEventThrottle={16}
    >
      {items.map((item, index) => (
        (index >= visibleRange.start - 2 && index <= visibleRange.end + 2) ? (
          <Item key={item.id} data={item} />
        ) : (
          <Placeholder key={item.id} height={ITEM_HEIGHT} />
        )
      ))}
    </ScrollView>
  );
};
Enter fullscreen mode Exit fullscreen mode

10. Use debounce to prevent extra renders & API calls

Delay processing until input stabilizes to avoid unnecessary operations.

import { debounce } from 'lodash';

const SearchBar = () => {
  const [query, setQuery] = useState('');

  const debouncedSearch = useCallback(
    debounce((searchTerm) => {
      performSearch(searchTerm);
    }, 300),
    []
  );

  const handleTextChange = (text) => {
    setQuery(text); // Update UI immediately
    debouncedSearch(text); // Delay API call
  };

  return (
    <TextInput
      value={query}
      onChangeText={handleTextChange}
      placeholder="Search..."
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

11. Use InteractionManager to defer non-essential tasks

Prioritize animations and user interactions by deferring background work.

const ImageGallery = () => {
  const [imagesLoaded, setImagesLoaded] = useState(false);

  useEffect(() => {
    // Show placeholders immediately
    setPlaceholderImages();

    // Load high-quality images after animations complete
    InteractionManager.runAfterInteractions(() => {
      loadHighQualityImages().then(() => {
        setImagesLoaded(true);
      });
    });
  }, []);

  return (
    <View>
      {imagesLoaded ? <HighQualityGallery /> : <PlaceholderGallery />}
    </View>
  );
};
Enter fullscreen mode Exit fullscreen mode

12. Use Fragments instead of unnecessary View wrappers

Reduce component tree depth by eliminating unneeded container views.

// Bad - extra view in the component tree
const UserInfo = () => (
  <View>
    <Text>Name: {user.name}</Text>
    <Text>Email: {user.email}</Text>
  </View>
);

// Good - no extra node in the component tree
const UserInfo = () => (
  <>
    <Text>Name: {user.name}</Text>
    <Text>Email: {user.email}</Text>
  </>
);
Enter fullscreen mode Exit fullscreen mode

13. Optimize FlatList configuration for visible items

Fine-tune FlatList parameters for your specific use case.

<FlatList
  data={items}
  renderItem={renderItem}
  horizontal
  pagingEnabled
  maxToRenderPerBatch={5}
  initialNumToRender={2}
  windowSize={3}
  removeClippedSubviews={true}
  getItemLayout={(data, index) => ({
    length: ITEM_WIDTH,
    offset: ITEM_WIDTH * index,
    index
  })}
/>
Enter fullscreen mode Exit fullscreen mode

For a horizontal FlatList with 20 items where only 2 are visible at a time and paging is enabled, these optimizations make a significant difference in scrolling performance.

Performance & Efficiency

14. Use FastImage

Replace the standard Image component for better loading, caching, and memory management.

import FastImage from 'react-native-fast-image';

const Avatar = ({ uri }) => (
  <FastImage
    source={{ 
      uri,
      priority: FastImage.priority.high,
      cache: FastImage.cacheControl.immutable
    }}
    style={styles.avatar}
    resizeMode={FastImage.resizeMode.cover}
  />
);
Enter fullscreen mode Exit fullscreen mode

15. Use FlashList instead of FlatList for better performance

FlashList offers significant performance improvements for long lists.

import { FlashList } from '@shopify/flash-list';

const ContactList = () => (
  <FlashList
    data={contacts}
    renderItem={({ item }) => <ContactItem contact={item} />}
    estimatedItemSize={75}
    keyExtractor={item => item.id}
  />
);
Enter fullscreen mode Exit fullscreen mode

16. Use Reanimated instead of Animated for animations

Reanimated runs animations on the UI thread for smoother performance.

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

const AnimatedButton = () => {
  const scale = useSharedValue(1);

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }]
  }));

  const handlePress = () => {
    scale.value = withSpring(0.9, {}, () => {
      scale.value = withSpring(1);
    });
  };

  return (
    <Pressable onPress={handlePress}>
      <Animated.View style={[styles.button, animatedStyle]}>
        <Text>Press Me</Text>
      </Animated.View>
    </Pressable>
  );
};
Enter fullscreen mode Exit fullscreen mode

17. Optimize local images

Compress images before including them in your app to improve load time and prevent FPS drops.

// Before adding images to your project:
// 1. Use tools like TinyPNG, ImageOptim, or Squoosh
// 2. Choose appropriate resolutions for different device densities
// 3. Consider using WebP format for better compression

// Bad: original.png (250KB)
// Good: optimized.png (85KB)

const Logo = () => (
  <Image 
    source={require('./assets/optimized.png')}
    style={styles.logo}
  />
);
Enter fullscreen mode Exit fullscreen mode

18. Avoid racing conditions

When calling the same API multiple times, ensure you only use the latest response.

const SearchScreen = () => {
  const [results, setResults] = useState([]);
  const requestIdRef = useRef(0);

  const search = async (query) => {
    // Generate unique request ID
    const requestId = ++requestIdRef.current;

    try {
      const data = await api.search(query);

      // Only update if this is still the latest request
      if (requestId === requestIdRef.current) {
        setResults(data);
      }
    } catch (error) {
      console.error('Search failed:', error);
    }
  };

  return (
    <View>
      <SearchInput onSearch={search} />
      <ResultsList results={results} />
    </View>
  );
};
Enter fullscreen mode Exit fullscreen mode

19. Use Promise.allSettled() for parallel API calls

Run multiple API calls in parallel to reduce total wait time.

// Bad: (3 + 5 = 8 seconds)
const loadData = async () => {
  const userData = await getUserData(); // 3 seconds
  const postsData = await getPostsData(); // 5 seconds
  return { userData, postsData };
};

// Good: (5 seconds total)
const loadData = async () => {
  const [userResult, postsResult] = await Promise.allSettled([
    getUserData(),
    getPostsData()
  ]);

  return {
    userData: userResult.status === 'fulfilled' ? userResult.value : null,
    postsData: postsResult.status === 'fulfilled' ? postsResult.value : []
  };
};
Enter fullscreen mode Exit fullscreen mode

20. Reduce bundle size

Remove unnecessary dependencies and unused code to decrease app size and loading time.

// Bad - importing entire library
import moment from 'moment';

// Good - import only what you need
import { format } from 'date-fns';

// Bad - importing all icons
import { Icon } from 'react-native-elements';

// Good - import specific icons
import HomeIcon from 'path-to-icons/Home';
import ProfileIcon from 'path-to-icons/Profile';
Enter fullscreen mode Exit fullscreen mode

21. Reduce API over-fetching

Implement pagination and partial data loading to improve initial load times.

const ProductFeed = () => {
  const [products, setProducts] = useState([]);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);

  const loadMore = async () => {
    if (!hasMore) return;

    const newProducts = await api.getProducts({
      page,
      limit: 20,
      fields: 'id,name,price,thumbnail' // Only fetch what you need
    });

    if (newProducts.length < 20) {
      setHasMore(false);
    }

    setProducts(prev => [...prev, ...newProducts]);
    setPage(prev => prev + 1);
  };

  return (
    <FlatList
      data={products}
      renderItem={renderProduct}
      onEndReached={loadMore}
      onEndReachedThreshold={0.5}
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

22. Implement optimistic UI updates

Update the UI immediately on user action, then revert if the API call fails.

const FavoriteButton = ({ product, initialIsFavorite }) => {
  const [isFavorite, setIsFavorite] = useState(initialIsFavorite);

  const toggleFavorite = async () => {
    // Update UI immediately
    setIsFavorite(current => !current);

    try {
      // Attempt to update on server
      await api.setFavorite(product.id, !isFavorite);
    } catch (error) {
      // If fails, revert the UI change
      setIsFavorite(initialIsFavorite);
      Alert.alert('Failed to update favorite status');
    }
  };

  return (
    <TouchableOpacity onPress={toggleFavorite}>
      <Icon name={isFavorite ? 'heart' : 'heart-outline'} />
    </TouchableOpacity>
  );
};
Enter fullscreen mode Exit fullscreen mode

23. Show cached data immediately while fetching

Implement a stale-while-revalidate pattern to improve perceived performance.

const NewsFeed = () => {
  const [news, setNews] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const [isRefreshing, setIsRefreshing] = useState(false);

  useEffect(() => {
    // Immediately load cached data
    AsyncStorage.getItem('@news_cache').then(cached => {
      if (cached) {
        setNews(JSON.parse(cached));
        setIsLoading(false);
      }

      // Then fetch fresh data
      refreshData();
    });
  }, []);

  const refreshData = async () => {
    setIsRefreshing(true);
    try {
      const freshNews = await api.getNews();
      setNews(freshNews);
      await AsyncStorage.setItem('@news_cache', JSON.stringify(freshNews));
    } catch (error) {
      console.error('Failed to refresh news:', error);
    } finally {
      setIsLoading(false);
      setIsRefreshing(false);
    }
  };

  if (isLoading && news.length === 0) {
    return <LoadingIndicator />;
  }

  return (
    <FlatList
      data={news}
      renderItem={renderNewsItem}
      refreshing={isRefreshing}
      onRefresh={refreshData}
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

Conclusion

These performance optimization techniques can dramatically improve your React Native app's speed, responsiveness, and user experience. Implement them strategically based on your specific app requirements and measurement results.

Remember: always measure performance before and after optimization to ensure your changes are having the intended effect. Premature optimization can sometimes add complexity without meaningful benefits.

By consistently applying these practices, you'll create React Native apps that not only work well but feel smooth and professional to your users.

Sentry blog image

The countdown to March 31 is on.

Make the switch from app center suck less with Sentry.

Read more

Top comments (0)

Billboard image

πŸ“Š A side-by-side product comparison between Sentry and Crashlytics

A free guide pointing out the differences between Sentry and Crashlytics, that’s it. See which is best for your mobile crash reporting needs.

See Comparison