Back to Journal
Mobile/Frontend

React Native Performance Best Practices for Startup Teams

Battle-tested best practices for React Native Performance tailored to Startup teams, including anti-patterns to avoid and a ready-to-use checklist.

Muneer Puthiya Purayil 18 min read

Startups building React Native apps need performance optimization strategies that deliver results without consuming weeks of engineering time. The focus should be on the highest-impact changes that improve user experience measurably, not micro-optimizations that complicate the codebase. These best practices are ordered by impact-per-effort ratio.

Startup Time Optimization

Enable Hermes and New Architecture

The single most impactful startup time improvement requires zero code changes:

json
1// android/gradle.properties
2hermesEnabled=true
3newArchEnabled=true
4 
5// iOS: Hermes is enabled by default in RN 0.70+
6 

Hermes reduces cold start time by 40-60% through bytecode precompilation. The New Architecture (Fabric renderer + TurboModules) reduces bridge overhead by 30%. Together, they transform a 3-second cold start into a 1.2-second cold start.

Optimize Bundle Size

bash
1# Analyze your bundle
2npx react-native-bundle-visualizer
3 
4# Common savings:
5# - Replace moment.js with date-fns (saves ~200KB)
6# - Replace lodash with individual imports (saves ~70KB)
7# - Use react-native-svg instead of embedding SVG images (saves ~50KB per image)
8 
typescript
1// Instead of importing the entire library
2import _ from 'lodash';
3_.debounce(fn, 300);
4 
5// Import only what you need
6import debounce from 'lodash/debounce';
7debounce(fn, 300);
8 

Defer Non-Critical Initialization

typescript
1import { InteractionManager } from 'react-native';
2 
3function App() {
4 useEffect(() => {
5 // Run after initial render and animations complete
6 InteractionManager.runAfterInteractions(() => {
7 initializeAnalytics();
8 initializePushNotifications();
9 prefetchUserData();
10 });
11 }, []);
12 
13 return <AppNavigator />;
14}
15 

List Performance

Use FlashList From Day One

typescript
1import { FlashList } from '@shopify/flash-list';
2 
3function FeedScreen() {
4 const renderItem = useCallback(
5 ({ item }: { item: FeedItem }) => <FeedCard item={item} />,
6 [],
7 );
8 
9 return (
10 <FlashList
11 data={feed}
12 renderItem={renderItem}
13 estimatedItemSize={200}
14 keyExtractor={item => item.id}
15 />
16 );
17}
18 
19// Memoize list items
20const FeedCard = React.memo(function FeedCard({ item }: { item: FeedItem }) {
21 return (
22 <View style={styles.card}>
23 <FastImage source={{ uri: item.image }} style={styles.image} />
24 <Text style={styles.title}>{item.title}</Text>
25 <Text style={styles.body}>{item.body}</Text>
26 </View>
27 );
28});
29 

FlashList uses cell recycling — it reuses off-screen component instances instead of creating new ones. This reduces memory pressure and eliminates GC pauses that cause scroll jank.

Paginate API Calls

typescript
1import { useInfiniteQuery } from '@tanstack/react-query';
2 
3function useFeed() {
4 return useInfiniteQuery({
5 queryKey: ['feed'],
6 queryFn: ({ pageParam = 0 }) => api.getFeed({ offset: pageParam, limit: 20 }),
7 getNextPageParam: (lastPage, pages) =>
8 lastPage.hasMore ? pages.length * 20 : undefined,
9 });
10}
11 
12function FeedScreen() {
13 const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useFeed();
14 
15 const items = data?.pages.flatMap(page => page.items) ?? [];
16 
17 return (
18 <FlashList
19 data={items}
20 renderItem={renderItem}
21 estimatedItemSize={200}
22 onEndReached={() => hasNextPage && fetchNextPage()}
23 onEndReachedThreshold={0.5}
24 ListFooterComponent={isFetchingNextPage ? <ActivityIndicator /> : null}
25 />
26 );
27}
28 

Image Optimization

Use FastImage Everywhere

typescript
1import FastImage from 'react-native-fast-image';
2 
3// Replace all Image components with FastImage
4// Before:
5// <Image source={{ uri: url }} style={styles.image} />
6 
7// After:
8<FastImage
9 source={{
10 uri: url,
11 priority: FastImage.priority.normal,
12 cache: FastImage.cacheControl.immutable,
13 }}
14 style={styles.image}
15 resizeMode={FastImage.resizeMode.cover}
16/>
17 

FastImage uses SDWebImage (iOS) and Glide (Android) under the hood, providing disk caching, memory caching, and progressive loading. The built-in Image component re-downloads images on every mount.

Resize Images Before Display

typescript
1// Use a CDN with image transformation (Cloudinary, imgix, Cloudflare Images)
2function getImageUrl(originalUrl: string, width: number, height: number): string {
3 return `${originalUrl}?w=${width * 2}&h=${height * 2}&fit=crop&auto=format`;
4 // 2x for retina displays
5}
6 
7function Avatar({ uri, size = 48 }: { uri: string; size?: number }) {
8 const optimizedUri = getImageUrl(uri, size, size);
9 return (
10 <FastImage
11 source={{ uri: optimizedUri }}
12 style={{ width: size, height: size, borderRadius: size / 2 }}
13 />
14 );
15}
16 

Animation Performance

Use Reanimated for Complex Animations

typescript
1import Animated, {
2 useSharedValue,
3 useAnimatedStyle,
4 withSpring,
5} from 'react-native-reanimated';
6 
7function AnimatedCard({ onPress }: { onPress: () => void }) {
8 const scale = useSharedValue(1);
9 
10 const animatedStyle = useAnimatedStyle(() => ({
11 transform: [{ scale: scale.value }],
12 }));
13 
14 const handlePressIn = () => {
15 scale.value = withSpring(0.95);
16 };
17 
18 const handlePressOut = () => {
19 scale.value = withSpring(1);
20 };
21 
22 return (
23 <Pressable onPressIn={handlePressIn} onPressOut={handlePressOut} onPress={onPress}>
24 <Animated.View style={[styles.card, animatedStyle]}>
25 {/* Card content */}
26 </Animated.View>
27 </Pressable>
28 );
29}
30 

Reanimated 3 runs animations entirely on the UI thread. The JS thread is never blocked, so animations remain at 60fps even during API calls or heavy computation.

Need a second opinion on your mobile/frontend architecture?

I run free 30-minute strategy calls for engineering teams tackling this exact problem.

Book a Free Call

State Management for Performance

typescript
1import { create } from 'zustand';
2 
3// Split stores by domain to prevent cross-domain re-renders
4const useAuthStore = create<AuthState>((set) => ({
5 user: null,
6 token: null,
7 login: async (credentials) => { /* ... */ },
8 logout: () => set({ user: null, token: null }),
9}));
10 
11const useFeedStore = create<FeedState>((set) => ({
12 items: [],
13 hasMore: true,
14 loadMore: async () => { /* ... */ },
15}));
16 
17// Select only what you need
18function HeaderUserName() {
19 const name = useAuthStore((s) => s.user?.name);
20 return <Text>{name ?? 'Guest'}</Text>;
21}
22 

Quick Performance Audit

Run this checklist on any React Native startup app:

typescript
1// performance-audit.ts
2const CHECKS = [
3 { name: 'Hermes enabled', check: () => typeof HermesInternal !== 'undefined' },
4 { name: 'Dev mode disabled', check: () => !__DEV__ },
5 { name: 'Console.log removed', check: () => !console.log.toString().includes('native') },
6];
7 
8function runAudit() {
9 for (const { name, check } of CHECKS) {
10 console.log(`${check() ? '✓' : '✗'} ${name}`);
11 }
12}
13 

Checklist

  • Hermes engine enabled
  • New Architecture enabled (Fabric + TurboModules)
  • Bundle analyzed and unnecessary dependencies removed
  • Non-critical initialization deferred with InteractionManager
  • FlashList used for all lists > 20 items
  • FastImage used for all network images
  • Reanimated 3 for animations (not Animated API)
  • React.memo on all list item components
  • Zustand with selectors for state management
  • React Query for API data with deduplication
  • console.log statements removed from production build

Anti-Patterns to Avoid

Over-memoizing: React.memo has a cost. Do not memoize components that are cheap to render and rarely receive the same props. Memoize list items and expensive computations, not every component.

Using Animated API for complex animations: The Animated API crosses the bridge for every frame update. Reanimated 3 runs on the UI thread natively. Switch for any animation beyond simple opacity/translation fades.

Storing derived data in state: If you can compute a value from existing state, compute it with useMemo instead of storing and syncing a separate state variable. This reduces state update frequency and prevents stale-derived-data bugs.

Ignoring Android performance: Test on mid-range Android devices ($200-300 price range), not just the latest iPhone. Android devices with 3-4GB RAM expose memory issues that iOS devices with 6-8GB hide.

Conclusion

Startup React Native performance optimization follows a straightforward priority order: enable Hermes (zero effort, 40% startup improvement), use FlashList and FastImage (low effort, dramatic list and image improvement), then address state management and animation (moderate effort, noticeable UX improvement).

Resist the temptation to prematurely optimize. Profile first — React DevTools Profiler and Flipper's performance plugin identify the actual bottlenecks, which are rarely where you expect them. Optimize the measured problem, not the hypothetical one.

FAQ

Need expert help?

Building with mobile/frontend?

I help teams ship production-grade systems. From architecture review to hands-on builds.

Muneer Puthiya Purayil

SaaS Architect & AI Systems Engineer. 10+ years shipping production infrastructure across fintech, automotive, e-commerce, and healthcare.

Engage

Start a
Conversation.

For teams building at scale: SaaS platforms, agentic AI systems, and enterprise mobile infrastructure. Scope and fit are evaluated before any engagement begins.

Limited availability · Q3 / Q4 2026