Back to Journal
Mobile/Frontend

React Native Performance at Scale: Lessons from Production

Real-world lessons from implementing React Native Performance in production, including architecture decisions, measurable results, and honest retrospectives.

Muneer Puthiya Purayil 12 min read

We migrated a consumer fintech app from React Native 0.68 to 0.73 with the New Architecture enabled, while simultaneously reducing cold start time by 62% and achieving consistent 60fps scrolling across 2,000+ device models. This case study documents the performance engineering decisions, measured results, and honest lessons over 8 months of production operation.

Context

The app — a personal finance management tool — had 180K monthly active users with a 4.2 App Store rating dragged down by performance complaints. The primary issues:

  • Cold start: 4.8 seconds on iPhone 12, 7.2 seconds on mid-range Android devices
  • List scroll jank: Feed of 500+ transactions dropped to 28fps during fast scrolling
  • Memory crashes: 12% of Android sessions experienced OOM kills
  • Navigation lag: Tab switches took 400-600ms with visible blank frames

Our team: 3 React Native developers, 1 iOS native developer, 1 Android native developer.

Phase 1: Foundation (Weeks 1-4)

Hermes and New Architecture Migration

The first month focused on upgrading React Native and enabling performance foundations:

bash
1# Upgrade path
2npx react-native upgrade 0.73
3# Enable Hermes (already default in 0.73)
4# Enable New Architecture
5 
json
1// android/gradle.properties
2newArchEnabled=true
3hermesEnabled=true
4 

Measured impact:

  • Cold start: 4.8s → 2.9s (40% reduction)
  • Memory usage at idle: 145MB → 98MB (32% reduction)
  • Bundle parse time: 1.2s → 180ms (Hermes bytecode precompilation)

Bundle Size Optimization

1Before: 12.4MB (JavaScript bundle)
2After: 7.8MB (37% reduction)
3 
4Key removals:
5- moment.js date-fns: -220KB
6- lodash (full) lodash-es (tree-shaken): -68KB
7- Unused Lottie animations: -1.2MB
8- Duplicate polyfills: -340KB
9 

Phase 2: List Performance (Weeks 5-8)

FlatList to FlashList Migration

The transaction feed was the most-used screen, displaying 500+ items with pull-to-refresh and infinite scroll:

typescript
1// Before: FlatList with 28fps during fast scroll
2<FlatList
3 data={transactions}
4 renderItem={renderTransaction}
5 keyExtractor={item => item.id}
6/>
7 
8// After: FlashList with 58fps during fast scroll
9<FlashList
10 data={transactions}
11 renderItem={renderTransaction}
12 estimatedItemSize={72}
13 keyExtractor={item => item.id}
14 getItemType={item => item.type} // credit, debit, transfer
15 overrideItemLayout={(layout, item) => {
16 layout.size = item.type === 'transfer' ? 96 : 72;
17 }}
18/>
19 

Measured results:

  • Scroll FPS: 28 → 58 (107% improvement)
  • Memory during scroll: 280MB → 120MB (57% reduction)
  • Cell blank rate: 15% → 0.3%

Transaction Row Optimization

typescript
1// Before: 14ms render time per cell
2function TransactionRow({ transaction }: Props) {
3 const formattedDate = format(new Date(transaction.date), 'MMM d, yyyy');
4 const formattedAmount = new Intl.NumberFormat('en-US', {
5 style: 'currency',
6 currency: 'USD',
7 }).format(transaction.amount);
8 
9 return (
10 <View style={styles.row}>
11 <Image source={{ uri: transaction.merchantLogo }} style={styles.logo} />
12 <View style={styles.details}>
13 <Text>{transaction.merchantName}</Text>
14 <Text>{formattedDate}</Text>
15 </View>
16 <Text>{formattedAmount}</Text>
17 </View>
18 );
19}
20 
21// After: 2ms render time per cell
22const TransactionRow = React.memo(function TransactionRow({ transaction }: Props) {
23 return (
24 <View style={styles.row}>
25 <FastImage
26 source={{ uri: transaction.merchantLogo, priority: FastImage.priority.low }}
27 style={styles.logo}
28 />
29 <View style={styles.details}>
30 <Text>{transaction.merchantName}</Text>
31 <Text>{transaction.formattedDate}</Text>
32 </View>
33 <Text style={transaction.amount > 0 ? styles.credit : styles.debit}>
34 {transaction.formattedAmount}
35 </Text>
36 </View>
37 );
38});
39 

Key changes:

  1. Pre-computed formattedDate and formattedAmount on the data layer, not in the render function
  2. Replaced Image with FastImage for disk caching
  3. Used React.memo to prevent re-renders when parent state changes
  4. Used StyleSheet.create for static styles (already was, but worth noting)

Phase 3: Navigation and Startup (Weeks 9-12)

typescript
1// Screen freeze: prevent background screens from processing updates
2import { enableFreeze } from 'react-native-screens';
3enableFreeze(true);
4 
5// Tab switch time: 450ms → 80ms
6 
7// Lazy load heavy screens
8const AnalyticsScreen = React.lazy(() => import('./screens/Analytics'));
9const SettingsScreen = React.lazy(() => import('./screens/Settings'));
10 

Startup Sequence Optimization

typescript
1// Before: everything initialized synchronously
2function App() {
3 initAnalytics(); // 200ms
4 initPushNotifications(); // 150ms
5 loadUserData(); // 300ms
6 initCrashReporting(); // 100ms
7 return <AppNavigator />;
8}
9 
10// After: critical path only, defer the rest
11function App() {
12 useEffect(() => {
13 // Deferred initialization
14 InteractionManager.runAfterInteractions(() => {
15 initAnalytics();
16 initPushNotifications();
17 });
18 }, []);
19 
20 return <AppNavigator />;
21}
22 
23// User data preloaded during splash screen
24// Crash reporting initialized in native code (before JS)
25 

Startup sequence results:

  • Cold start (iPhone 12): 2.9s → 1.8s
  • Cold start (mid-range Android): 5.1s → 2.8s
  • Time to interactive: 3.2s → 1.4s

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

Phase 4: Memory Optimization (Weeks 13-16)

Image Cache Management

typescript
1// Limit FastImage disk cache to 200MB
2FastImage.clearDiskCache();
3 
4// Implement LRU eviction
5const IMAGE_CACHE_SIZE_MB = 200;
6const IMAGE_CACHE_MAX_AGE_DAYS = 7;
7 

Memory Monitoring in Production

typescript
1// Integrated with DataDog RUM
2function reportMemoryMetrics() {
3 const memInfo = NativeModules.MemoryInfo;
4 if (memInfo) {
5 const usage = memInfo.getMemoryUsageSync();
6 datadogRum.addAction('memory_sample', {
7 usedMB: Math.round(usage.usedMemory / 1024 / 1024),
8 totalMB: Math.round(usage.totalMemory / 1024 / 1024),
9 });
10 }
11}
12 
13// Sample every 30 seconds
14setInterval(reportMemoryMetrics, 30000);
15 

Memory results:

  • Average session memory: 145MB → 88MB
  • OOM crash rate: 12% → 0.8%
  • Background memory: 95MB → 42MB

Production Results (8-Month Summary)

MetricBeforeAfterImprovement
Cold start (iOS)4.8s1.8s62% faster
Cold start (Android)7.2s2.8s61% faster
Transaction list FPS2858107%
Memory usage (avg)145MB88MB39% less
OOM crash rate12%0.8%93% reduction
App Store rating4.24.6+0.4 stars
1-star reviews (perf)23/month3/month87% reduction

What We Would Do Differently

  1. Start with FlashList: We spent 3 weeks tuning FlatList before switching to FlashList, which solved most issues immediately. Start with FlashList from day one.

  2. Measure before optimizing: Our first month included "optimizations" based on intuition that profiling later showed had zero impact. Use React DevTools Profiler and Flipper from the start.

  3. Test on low-end devices earlier: Our initial testing was on iPhone 13 and Pixel 6. The worst performance issues only appeared on devices with 3GB RAM, which represent 40% of our Android user base.

  4. Pre-compute formatted data: Moving date and currency formatting from render functions to the data transformation layer was the highest-ROI change. Every formatter call was executing on every render of every list cell.

Conclusion

The 62% cold start improvement and 107% scroll FPS improvement came from well-known techniques applied systematically: Hermes, FlashList, FastImage, screen freezing, and deferred initialization. No exotic optimizations were needed. The challenge was not knowing what to do — it was measuring correctly, prioritizing based on data, and resisting the urge to optimize prematurely.

The App Store rating increase from 4.2 to 4.6 translated directly to a 15% improvement in organic installs. Performance is a feature with measurable business impact.

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