It's Not React Native's Fault (Usually)
When clients come to us saying "React Native is slow, should we rewrite in native?", our first response is to do a performance audit. In about 80% of cases, the performance issues are caused by common antipatterns in the application code, not limitations of React Native. A rewrite would carry these same patterns into a new codebase and not actually fix anything.
Here are the actual causes we find in almost every React Native performance audit.
FlatList Misuse: The #1 Offender
We cannot overstate how often this is the problem. The app has a list of 500+ items and it re-renders the entire list on every state change. Classic symptoms: scrolling feels janky, the app uses too much memory, interactions are delayed.
The fixes we apply in order: First, ensure every list item has a stable, unique keyExtractor (not the index). Second, wrap list item components in React.memo and ensure they don't reference parent state that changes frequently. Third, set getItemLayout if your items have a fixed height — this eliminates the layout calculation phase. Fourth, increase windowSize if you're seeing blank areas during fast scrolling (default is 21, we often bump to 30-40 for complex items). Fifth, use maxToRenderPerBatch to control how many items render per frame (we typically set this to 10-15).
For one client, applying just the first three fixes reduced list rendering time by 73%. Their "slow app" became a fast app in about two hours of work.
Unnecessary Re-renders: The Silent Killer
Install the React DevTools Profiler and enable "Highlight updates when components re-render." You'll probably be horrified. In one audit, we found that a header component with a notification badge was re-rendering 15 times per second because it was subscribed to a Redux store that updated on every incoming WebSocket message. The fix: use a selector that only returns the notification count, not the entire message list.
We use the why-did-you-render library on every React Native project during development. It logs unexpected re-renders and the reason they happened. Common culprits: creating new objects or arrays in render (causing reference inequality), using inline functions as props (new function reference every render), and missing useMemo/useCallback on expensive computations or callback props.
Image Loading: The Invisible Performance Tax
React Native's built-in Image component is basic. For apps that display many images (social feeds, e-commerce catalogs, image galleries), it's not good enough. It doesn't cache aggressively, it doesn't support progressive loading, and it doesn't optimize memory usage for large images.
We replaced the Image component with react-native-fast-image on a social app and saw: 40% reduction in memory usage (it properly recycles bitmap memory), 2x faster image loading on subsequent views (aggressive disk and memory caching), and elimination of the "white flash" when scrolling through image-heavy feeds (proper placeholder and fade-in support). This library is almost always our first addition to any React Native project.
Navigation Performance
If screen transitions feel slow, check your navigation setup. React Navigation's default behavior renders all screens in a stack simultaneously. For a stack with 5 screens, that's 5 component trees in memory. Use the lazy option in tab navigators (only render a tab when first visited) and enableFreeze from react-native-screens (freezes screens that aren't visible, preventing re-renders).
We also see slow initial screen load times caused by importing heavy dependencies at the top of the file. If a screen imports a PDF viewer library that's 500KB, that import happens when the component module is first referenced, not when the screen is visible. Use dynamic imports (React.lazy) for heavy screen-specific dependencies.
The New Architecture Helps, But Isn't Magic
React Native's new architecture (Fabric renderer + TurboModules + JSI) is a genuine improvement, especially for native module calls. JSI eliminates the JSON serialization overhead of the old bridge, which helps with high-frequency native calls (animations, gestures). We've measured about a 15-20% improvement in animation smoothness after migrating to the new architecture on one project.
But the new architecture doesn't fix application-level antipatterns. If your component re-renders 500 times per second, JSI won't save you. Fix the fundamentals first, upgrade the architecture second.