When we started building Petinom, our pet care platform, performance was a top priority. Mobile users are impatient - research shows that 53% of users abandon an app if it takes longer than 3 seconds to load. For a pet care app where owners might be anxiously checking their pet's health records, every millisecond matters.
React Native performance optimization isn't about applying a checklist of tricks. It's about understanding the bridge between JavaScript and native code, knowing when the main thread is blocked, and measuring everything before and after each change. Here are the key insights.
Lazy Loading and Code Splitting
A growing JavaScript bundle is one of the first performance bottlenecks in any React Native app. Mid-range Android devices are especially sensitive to large initial bundles. Profiling the startup sequence often reveals that many screens are loaded upfront even though most users don't visit them on every session.
Implementing React.lazy() with custom loading states for each screen can dramatically reduce the initial bundle size. Combined with route-based code splitting for the navigation stack, each screen group loads independently, leading to noticeably faster startup times - especially on mid-range devices.
The key lesson is that not all lazy loading is equal. Measuring user flows helps prioritize the hot path - the screens users visit most frequently should be loaded eagerly, while everything else loads on demand.
Optimized List Rendering
Apps that handle health records, vaccination schedules, and activity histories can accumulate hundreds of entries per user. The built-in FlatList component can struggle with large datasets, leading to visible jank and frame drops during scrolling.
FlashList from Shopify is a strong alternative. It recycles views like a native RecyclerView/UICollectionView, creating far fewer component instances. The result is smooth 60 FPS scrolling even with large datasets.
But FlashList alone isn't always enough. Component architecture matters too. If list items re-render on every scroll due to inline styles and anonymous function props, performance suffers regardless of the list implementation. Extracting components, memoizing with React.memo(), and using useCallback() for handlers can dramatically reduce unnecessary re-renders.
Image Optimization
In image-heavy apps, users can accumulate dozens of photos that need to be displayed in grids and galleries. Without optimization, this leads to memory pressure and scroll jank.
A three-tier image strategy works well here: tiny blurred placeholders load instantly and provide a color preview, medium thumbnails load for list views, and full-resolution images load only when the user taps to view details. Libraries like expo-image handle disk and memory cache layers automatically.
This approach significantly reduces both perceived load time and memory usage on image-heavy screens.
Memory Management
Memory leaks in React Native are subtle and dangerous. They don't crash the app immediately - instead, the app gradually slows down over a session until the OS kills it. We established strict patterns for cleanup: every useEffect that creates a subscription returns a cleanup function, every timer is cleared, and every event listener is removed.
A useful pattern is building a custom hook that pauses background operations when the app is minimized - stopping location tracking, pausing real-time sync, and reducing timer frequency. On Android, where aggressive battery optimization can kill background processes, this approach is essential for reliability.
