Before you optimize
Measure first. Turn on the React Native DevTools performance monitor. Profile on a real mid-tier Android device (not your iPhone 15 Pro Max). Identify the actual bottleneck. Fix what’s slow, not what feels slow.
1. Swap FlatList for FlashList
Shopify’s FlashList is a near-drop-in replacement that’s dramatically faster for long lists. Memory usage is lower, scroll stays at 60fps on mid-tier devices, and mixed-size items render far more smoothly. Install @shopify/flash-list, change the import, add an estimated item size.
2. Use MMKV instead of AsyncStorage
react-native-mmkv is roughly 30x faster than AsyncStorage and supports synchronous reads. For any app with local state the user expects to see instantly (list of tasks, cached profile, feature flags), switching to MMKV removes visible jank on app open. AsyncStorage should be a fallback, not the default.
3. Use Reanimated over the legacy Animated API
react-native-reanimated runs animations on the UI thread, so they stay smooth even when the JS thread is busy. The legacy Animated API runs on the JS thread and stutters during work. Any gesture-driven animation, complex transition, or layout animation should be on Reanimated in 2026.
4. Use expo-image with caching
expo-image is the 2026 default — faster decoding, better memory behavior, and built-in disk caching. Replace the built-in Image component for anything network-loaded. Set cachePolicy="memory-disk" and contentFit explicitly.
5. Memoize heavy list rows
Each row re-render costs real time on mid-tier devices. Wrap row components in React.memo, pass stable callbacks via useCallback, and extract derived values with useMemo. Don’t blanket-memoize — start with the row and its immediate children. Profile to confirm the render count dropped.
6. Confirm Hermes is on in production
Hermes is now the default engine for new Expo projects. For older projects, set it explicitly:
// app.json
{
"expo": {
"jsEngine": "hermes"
}
}Also confirm EAS Build is using production (not dev) mode — dev bundles are 2–3x larger and slower.
7. Keep JSON parsing off the main render path
Parsing large JSON payloads in render functions (or even in useEffect) blocks the JS thread.JSON.parse on a 500KB response freezes the UI on mid-tier devices. Parse in a worker, stream-parse if possible, or paginate the API so responses stay small.
8. Lazy-load screens with Expo Router
Expo Router lazy-loads route groups by default, but you still want to split heavy libraries (charts, video players, PDF viewers) into dynamic imports so they don’t load on every app start. Use React.lazy() for rarely-used screens and watch your time-to-interactive.
9. Defer work with InteractionManager
When you navigate to a new screen, the transition animation runs on the main thread. Any heavy work during that window (data fetching, sync, cache reconciliation) causes visible stutter. Wrap it in InteractionManager.runAfterInteractions(() => ...) to let the animation complete first.
10. Strip dev-only code from production
Dev-only imports (Reactotron, logging libs, debug UIs) should not ship. Check via:
- Wrap dev imports in
if (__DEV__) {}. - Audit bundle size with
npx expo export --platform iosand inspect the output. - Check for stray
console.logcalls — they slow Hermes measurably in tight loops.
Tools to profile before and after
- React Native DevTools — built-in performance monitor and profiler in 2026.
- Flipper (if still used) — network, layout, and React tree inspection.
- Sentry — production performance traces and slow-render detection.
- Xcode Instruments — for deep iOS profiling.
- Android Studio Profiler — for Android-specific work.
AI-generated apps: the default audit
After generating an app via ShipNative or similar, run through this quick audit: (1) replace FlatList with FlashList, (2) add expo-image for network images, (3) confirm Hermes is on, (4) memoize any screen that renders 20+ items. That’s usually enough to take the app from “works” to “feels great.”