Why Performance Matters More Than Ever in React Native
React Native has come a long way in the past few years, and honestly, 2026 feels like a turning point for mobile performance. The New Architecture is enabled by default now, Hermes V1 is on the horizon, and the React Compiler is automating optimizations that we used to handle manually. The performance landscape has fundamentally shifted.
But here's the thing — the gap between a sluggish app and a buttery-smooth 60 FPS experience still comes down to the choices you make as a developer.
This guide is a practical walkthrough of every major performance optimization technique available to React Native developers in 2026. Whether you're building a new app from scratch or trying to squeeze more speed out of an existing one, you'll find actionable strategies backed by real benchmarks and production-tested patterns. We'll cover the full stack — from architecture-level decisions down to individual component tweaks — so you can ship apps that feel truly native.
The New Architecture: Your Performance Foundation
The single biggest performance lever in React Native today is the New Architecture. If you're still running on the old bridge-based system, migrating should be your top priority. As of early 2026, roughly 83% of Expo SDK 54 projects built with EAS Build already use the New Architecture — and for good reason.
Understanding the Performance Gains
The old architecture relied on a JSON-serialized bridge to communicate between JavaScript and native code. Every interaction — layout calculations, native module calls, event handling — had to be serialized, sent across the bridge, deserialized, and processed. It was a lot of overhead, to put it mildly.
The New Architecture eliminates this bottleneck through three key technologies:
- JSI (JavaScript Interface): JavaScript can now hold direct references to C++ host objects. Your JS code talks to native modules almost instantly, without any JSON serialization overhead.
- Fabric: The new rendering system supports concurrent rendering, synchronous layout measurements, and priority-based updates. Components can render on the main thread when needed for responsive interactions.
- TurboModules: Native modules load lazily on demand instead of all at startup, and they communicate through JSI rather than the bridge. This alone can cut startup time significantly.
The combined effect is pretty transformative. Apps running the New Architecture typically see a 30–50% reduction in Time-to-Interactive (TTI), smoother animations, and more responsive touch handling. Bridgeless mode takes it even further by eliminating all bridge initialization — error handling, timers, and global event emitters all run natively through JSI.
Enabling the New Architecture
If you're using Expo SDK 54 or later, the New Architecture is enabled by default. For bare React Native projects on 0.79+, enable it in your configuration:
// app.json (Expo)
{
"expo": {
"newArchEnabled": true
}
}
// For bare React Native on Android (gradle.properties)
newArchEnabled=true
// For bare React Native on iOS (Podfile)
ENV['RCT_NEW_ARCH_ENABLED'] = '1'
After enabling, rebuild your app completely. Run npx react-native-new-arch-helper check to verify that all your dependencies support the New Architecture. Libraries that haven't migrated to TurboModules will fall back to an interop layer, which works but doesn't deliver the full performance benefits.
Hermes V1: The Next-Generation JavaScript Engine
Hermes has been the default JavaScript engine for React Native since version 0.70, but Hermes V1 — introduced experimentally in React Native 0.82 — is a generational leap. If you're on React Native 0.82 or later, you can opt into it today.
What Hermes V1 Brings
Hermes V1 delivers improved VM performance across the board. You get better support for modern JavaScript features including proper ES6 classes, const and let scoping, and enhanced async/await handling. The engine is faster at parsing, compiling, and executing JavaScript, which translates directly into faster startup and smoother runtime performance.
The roadmap is even more exciting, honestly. Future releases plan to introduce JIT (Just-In-Time) compilation, the ability to compile JavaScript bundles to native binaries, and type-informed optimizations that leverage TypeScript annotations for faster execution. These features aren't available yet, but adopting Hermes V1 now positions your app to benefit from them the moment they land.
Verifying Hermes Is Active
You can check that Hermes is running with a simple runtime check:
// Check Hermes status at runtime
const isHermes = () => {
return typeof HermesInternal !== 'undefined';
};
console.log(`Running on Hermes: ${isHermes()}`);
// Access Hermes-specific performance info
if (isHermes()) {
const heapInfo = HermesInternal?.getInstrumentedStats?.();
console.log('Heap size:', heapInfo?.js_heapSize);
}
Expo projects using SDK 54+ have Hermes enabled by default. For bare projects, verify your build.gradle includes hermesEnabled: true and your Podfile doesn't override the Hermes setting.
React Compiler: Automatic Memoization in Production
The React Compiler hit version 1.0 in late 2025, and it's one of the most impactful performance tools for React Native developers right now. It's a build-time optimization tool that automatically memoizes your components and hooks — eliminating the need for manual React.memo, useMemo, and useCallback in most cases.
How the Compiler Optimizes Your Code
The React Compiler works as a Babel plugin that analyzes your component code at build time. It lowers your code into a novel High-level Intermediate Representation (HIR) and performs multiple compiler passes to understand data flow and mutability patterns. Based on this analysis, it granularly memoizes values used in rendering — including conditional memoization, which is basically impossible to do well with manual approaches.
In production at Meta, the React Compiler has demonstrated up to 12% faster page loads and 2.5x quicker user interactions. These aren't synthetic benchmarks — they come from apps serving billions of users.
Enabling the React Compiler
Expo SDK 54 and later enables the React Compiler by default. For other setups, install and configure it manually:
npm install -D babel-plugin-react-compiler
// babel.config.js
module.exports = {
plugins: [
['babel-plugin-react-compiler', {
// Target React Native runtime
target: '19', // React 19+
}],
],
};
Writing Compiler-Friendly Code
While the compiler handles most optimizations automatically, certain patterns help it generate better output. So, let's look at what works well and what doesn't:
// Good: Pure component logic the compiler can optimize
function ProductCard({ product, onPress }) {
const formattedPrice = `$${product.price.toFixed(2)}`;
const inStock = product.inventory > 0;
return (
<Pressable onPress={() => onPress(product.id)}>
<Text>{product.name}</Text>
<Text>{formattedPrice}</Text>
{inStock ? <Badge text="In Stock" /> : <Badge text="Sold Out" />}
</Pressable>
);
}
// Avoid: Side effects during render confuse the compiler
function BadComponent({ data }) {
// Don't mutate external variables during render
globalCounter++; // The compiler cannot safely memoize this
// Don't read from mutable refs during render for display
const val = someRef.current; // Unstable dependency
return <Text>{val}</Text>;
}
You don't need to remove existing useMemo and useCallback calls — the compiler respects them. But for new code, just write straightforward components and let the compiler do its thing.
List Performance: FlashList, Legend List, and Beyond
Lists are the most common performance bottleneck in mobile apps. A feed, a product catalog, a chat view — if your list stutters during scrolling, users notice immediately. I've personally spent more debugging hours on list performance than almost any other issue in React Native.
React Native's built-in FlatList works for simple cases, but for production apps with hundreds or thousands of items, you need a better solution.
FlashList: The Production Standard
FlashList, created by Shopify, replaces FlatList's virtualization strategy with cell recycling. Instead of destroying and recreating components as they scroll off-screen, FlashList maintains a fixed pool of component instances and swaps their data. This dramatically reduces garbage collection pressure and JavaScript thread work.
The real-world benchmarks tell the story: teams switching from FlatList to FlashList consistently report 54% FPS improvements (jumping from ~37 FPS to ~57 FPS), 82% CPU reduction on the JS thread, and elimination of out-of-memory crashes on low-end Android devices. FlashList v2, rebuilt for the New Architecture, eliminates the need for item size estimates entirely.
import { FlashList } from "@shopify/flash-list";
function ProductFeed({ products }) {
const renderItem = useCallback(({ item }) => (
<ProductCard product={item} />
), []);
return (
<FlashList
data={products}
renderItem={renderItem}
estimatedItemSize={120}
keyExtractor={(item) => item.id}
// Optimize for specific layout
numColumns={2}
// Draw distance controls how far ahead items render
drawDistance={250}
/>
);
}
Legend List: The New Contender
Legend List from LegendApp has emerged as a strong alternative — particularly for complex UI scenarios. Benchmarks show it uses less CPU and memory than FlashList during scrolling, making it especially attractive for media-heavy feeds, real-time updating lists, and layouts with dynamically sized items.
import { LegendList } from "@legendapp/list";
function ChatMessages({ messages }) {
return (
<LegendList
data={messages}
renderItem={({ item }) => <ChatBubble message={item} />}
keyExtractor={(item) => item.id}
// Legend List handles dynamic heights natively
estimatedItemSize={80}
// Inverted for chat-style bottom-to-top scrolling
inverted
// Recycle items for performance
recycleItems
/>
);
}
Choosing the Right List Component
Here's a practical decision framework:
- FlatList: Short lists (under 100 items) with simple layouts. No extra dependency needed.
- FlashList: Medium to large lists where you need proven stability, great documentation, and broad community support. Excellent for e-commerce catalogs, social feeds, and standard data lists.
- Legend List: Complex UIs with multiple nested lists, media-heavy content, real-time updates, or dynamically sized items where you need maximum performance with minimal config.
Universal List Optimization Tips
Regardless of which list component you choose, these optimizations apply across the board:
// 1. Stabilize renderItem with useCallback
const renderItem = useCallback(({ item }) => (
<MemoizedListItem item={item} />
), []);
// 2. Use getItemLayout when items have fixed heights
const getItemLayout = useCallback((data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
}), []);
// 3. Optimize the keyExtractor
const keyExtractor = useCallback((item) => item.id.toString(), []);
// 4. Reduce re-renders with stable references
// Bad: creates a new object every render
<FlatList contentContainerStyle={{ padding: 16 }} />
// Good: stable style reference
const styles = StyleSheet.create({
container: { padding: 16 },
});
<FlatList contentContainerStyle={styles.container} />
Image Optimization: Fast Loading Without the Memory Cost
Images are typically the largest assets your app loads, and poorly optimized image handling is one of the most common causes of memory issues and slow perceived performance. In 2026, two libraries dominate the React Native image landscape.
expo-image: The Modern Standard
If you're using Expo (and at this point, you probably should be), expo-image is the recommended solution. It provides aggressive caching, automatic resizing to container dimensions, blur hash placeholders, and support for modern formats like WebP and AVIF.
The automatic resizing is particularly valuable — it reduces memory usage by ensuring the decoded image in memory matches the display size rather than the source size. That might sound like a small thing, but when you're rendering a 4000x3000 photo in a 200x200 thumbnail, the memory difference is enormous.
import { Image } from 'expo-image';
function OptimizedProductImage({ uri, blurhash }) {
return (
<Image
source={{ uri }}
// Blur hash shows instantly while image loads
placeholder={{ blurhash }}
// Smooth transition from placeholder
transition={300}
// Resize to container, saving memory
contentFit="cover"
// Cache aggressively
cachePolicy="memory-disk"
// Enable recycling in lists
recyclingKey={uri}
style={{ width: '100%', aspectRatio: 1 }}
/>
);
}
Image Optimization Best Practices
Beyond choosing the right library, follow these practices for optimal image performance:
- Use WebP format: WebP images are 25–35% smaller than equivalent JPEG or PNG files with no visible quality loss. Most CDNs can convert images to WebP on the fly.
- Serve correctly sized images: If your image displays at 200x200 pixels on a 3x density screen, request a 600x600 image from your CDN — not the original 4000x3000 photo.
- Prefetch strategically: In lists, prefetch images for items just beyond the visible area. Both
expo-imageandreact-native-fast-imagesupport prefetching APIs. - Use priority loading: Assign high priority to images in the initial viewport (hero images, profile avatars) and normal or low priority to off-screen content.
import { Image } from 'expo-image';
// Prefetch upcoming images in a list
async function prefetchNextPage(imageUrls) {
await Promise.all(
imageUrls.map(url => Image.prefetch(url))
);
}
// In your list component
function ImageFeed({ pages, currentPage }) {
useEffect(() => {
const nextPage = pages[currentPage + 1];
if (nextPage) {
prefetchNextPage(nextPage.map(item => item.imageUrl));
}
}, [currentPage]);
// ... render list
}
Bundle Size: Shipping Less JavaScript
Every kilobyte of JavaScript your app ships must be parsed, compiled, and executed before your app becomes interactive. Reducing bundle size directly improves startup time, and in 2026 there are some powerful tools to help.
Analyzing Your Bundle
Before optimizing, measure. Use react-native-bundle-visualizer to create a visual map of your bundle composition:
npx react-native-bundle-visualizer
# For Expo projects
npx expo export --dump-sourcemap
npx source-map-explorer dist/bundles/ios-*.js
A typical finding (and I've seen this over and over): 30–40% of your bundle comes from just a few large dependencies. Identifying and addressing these outliers delivers outsized improvements.
Practical Bundle Reduction Strategies
Start with the highest-impact changes:
- Audit your dependencies: Run
npx depcheckto find unused packages. It's surprisingly common to find libraries imported during development that never made it to production code. - Choose lighter alternatives: Replace
moment.js(~300KB) withdate-fns(~20KB for typical usage) or the nativeIntl.DateTimeFormatAPI. Replacelodashwith individual function imports or native methods. - Tree-shake aggressively: Use named imports (
import { map } from 'lodash-es') instead of default imports (import _ from 'lodash') to enable tree shaking. - Lazy-load heavy screens: Screens the user doesn't see immediately should load on demand.
import { lazy, Suspense } from 'react';
import { ActivityIndicator } from 'react-native';
// Lazy load heavy screens
const AnalyticsDashboard = lazy(
() => import('./screens/AnalyticsDashboard')
);
const SettingsScreen = lazy(
() => import('./screens/SettingsScreen')
);
// Use Suspense for loading states
function AppNavigator() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Analytics">
{() => (
<Suspense fallback={<ActivityIndicator size="large" />}>
<AnalyticsDashboard />
</Suspense>
)}
</Stack.Screen>
</Stack.Navigator>
);
}
Metro Bundler Optimizations
Metro, React Native's bundler, has some configuration options that can shave real kilobytes off your bundle:
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const config = getDefaultConfig(__dirname);
// Enable minification optimizations
config.transformer.minifierConfig = {
compress: {
drop_console: true, // Remove console.log in production
drop_debugger: true, // Remove debugger statements
pure_getters: true,
passes: 2, // Multiple optimization passes
},
mangle: {
toplevel: true,
},
};
module.exports = config;
Rendering Performance: Avoiding Unnecessary Re-renders
Even with the React Compiler handling memoization, understanding re-render patterns helps you write components that are inherently efficient. The compiler optimizes what you give it — better input produces better output.
Component Architecture for Performance
The most effective re-render optimization is structural: isolate state to the smallest possible component tree. This is one of those things that sounds obvious but makes a huge difference in practice.
// Bad: Entire screen re-renders when timer ticks
function ProductScreen({ product }) {
const [timeLeft, setTimeLeft] = useState(calculateTimeLeft());
useEffect(() => {
const timer = setInterval(() => {
setTimeLeft(calculateTimeLeft());
}, 1000);
return () => clearInterval(timer);
}, []);
return (
<ScrollView>
<ProductImage uri={product.imageUrl} />
<ProductDetails product={product} />
<Text>Sale ends in: {timeLeft}</Text> {/* Only this changes */}
<RelatedProducts categoryId={product.categoryId} />
</ScrollView>
);
}
// Good: Only the countdown re-renders
function ProductScreen({ product }) {
return (
<ScrollView>
<ProductImage uri={product.imageUrl} />
<ProductDetails product={product} />
<CountdownTimer /> {/* Isolated state */}
<RelatedProducts categoryId={product.categoryId} />
</ScrollView>
);
}
function CountdownTimer() {
const [timeLeft, setTimeLeft] = useState(calculateTimeLeft());
useEffect(() => {
const timer = setInterval(() => {
setTimeLeft(calculateTimeLeft());
}, 1000);
return () => clearInterval(timer);
}, []);
return <Text>Sale ends in: {timeLeft}</Text>;
}
Expensive Computations
When you have genuinely expensive computations that the compiler might not optimize perfectly, explicit memoization is still the way to go:
function SearchResults({ query, products }) {
// Expensive: filtering and sorting thousands of items
const filteredProducts = useMemo(() => {
return products
.filter(p => p.name.toLowerCase().includes(query.toLowerCase()))
.sort((a, b) => b.relevanceScore - a.relevanceScore)
.slice(0, 100);
}, [query, products]);
return <FlashList data={filteredProducts} /* ... */ />;
}
Animation Performance: Staying on the UI Thread
Animations that run on the JavaScript thread compete with your business logic, network requests, and state management for processing time. The result? Dropped frames and janky transitions. Nobody wants that.
The solution is to keep animations on the UI thread.
Reanimated for UI-Thread Animations
React Native Reanimated runs animations directly on the UI thread using worklets — small JavaScript functions that execute outside the main JS context. Combined with Reanimated 4's CSS Animations support, you've got multiple approaches depending on complexity:
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
} from 'react-native-reanimated';
function AnimatedCard({ isExpanded }) {
const height = useSharedValue(80);
useEffect(() => {
height.value = withSpring(isExpanded ? 200 : 80, {
damping: 15,
stiffness: 150,
});
}, [isExpanded]);
const animatedStyle = useAnimatedStyle(() => ({
height: height.value,
overflow: 'hidden',
}));
return (
<Animated.View style={animatedStyle}>
{/* Card content */}
</Animated.View>
);
}
For simpler transitions, Reanimated 4's CSS-style API is even cleaner and performs identically (since it also runs on the UI thread):
import Animated from 'react-native-reanimated';
// CSS Transitions — declare once, animate automatically
const animatedStyle = {
height: isExpanded ? 200 : 80,
transitionProperty: 'height',
transitionDuration: '300ms',
transitionTimingFunction: 'ease-in-out',
};
<Animated.View style={animatedStyle} />
Gesture-Driven Animations
For gesture-driven animations (swipeable cards, pull-to-refresh, pinch-to-zoom), always pair react-native-gesture-handler with Reanimated to keep the entire gesture-to-animation pipeline on the UI thread:
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
function SwipeableCard() {
const translateX = useSharedValue(0);
const panGesture = Gesture.Pan()
.onUpdate((event) => {
translateX.value = event.translationX;
})
.onEnd(() => {
translateX.value = withSpring(0);
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
return (
<GestureDetector gesture={panGesture}>
<Animated.View style={animatedStyle}>
{/* Card content */}
</Animated.View>
</GestureDetector>
);
}
Memory Management: Preventing Leaks Before They Start
Memory leaks in React Native are insidious. They don't throw errors, don't show warnings, and won't crash your app immediately. Instead, they slowly degrade performance until the OS eventually kills your app. Fun times.
The best approach is prevention through architecture rather than detection through debugging.
Common Memory Leak Patterns
These are the most frequent causes of memory leaks I've encountered in React Native apps:
// Leak: Timer not cleaned up
function LeakyComponent() {
useEffect(() => {
const interval = setInterval(fetchData, 5000);
// Missing: return () => clearInterval(interval);
}, []);
}
// Leak: Event listener not removed
function LeakyListener() {
useEffect(() => {
const subscription = AppState.addEventListener('change', handler);
// Missing: return () => subscription.remove();
}, []);
}
// Leak: Setting state on unmounted component
function LeakyAsync() {
const [data, setData] = useState(null);
useEffect(() => {
let cancelled = false;
fetchData().then(result => {
if (!cancelled) {
setData(result); // Safe: only set if still mounted
}
});
return () => { cancelled = true; };
}, []);
}
Production Memory Monitoring
Use Hermes's built-in memory tracking to monitor heap usage in development:
// Monitor memory in development
function useMemoryMonitor(screenName) {
useEffect(() => {
if (__DEV__ && typeof HermesInternal !== 'undefined') {
const before = HermesInternal?.getInstrumentedStats?.();
return () => {
// Force garbage collection
HermesInternal?.collectGarbage?.();
const after = HermesInternal?.getInstrumentedStats?.();
const heapGrowth = (after?.js_heapSize || 0) - (before?.js_heapSize || 0);
if (heapGrowth > 1_000_000) { // More than 1MB growth
console.warn(
`[Memory] ${screenName} may have a leak: ` +
`heap grew by ${(heapGrowth / 1_000_000).toFixed(2)}MB`
);
}
};
}
}, [screenName]);
}
Profiling and Measuring Performance
You can't optimize what you don't measure. React Native provides several tools for profiling performance, and using them should be a regular part of your development workflow — not just when something feels slow.
The React Native Performance Monitor
The simplest starting point is the built-in performance monitor. On a dev build, shake your device (or press Cmd+D in the simulator) and enable "Show Perf Monitor." This overlay shows real-time FPS for both the UI thread and JS thread. Your target: both threads should stay at or near 60 FPS during interactions.
React DevTools Profiler
The React DevTools Profiler shows exactly which components rendered and why. Connect React DevTools to your app and record a profiling session during the interaction you want to optimize:
# Start React DevTools
npx react-devtools
# Or use the built-in Expo Dev Tools
npx expo start --dev-client
Look for components that render repeatedly without their props changing — those are your prime candidates for state isolation or memoization. The "Why did this render?" feature in the Profiler settings is particularly useful for tracking down unnecessary re-renders.
Flipper and Platform-Specific Profiling
For deeper analysis, reach for platform-specific tools:
- Flipper: Meta's debugging platform includes a performance plugin that visualizes JS thread and UI thread activity on a timeline. It's excellent for identifying bridge calls, layout thrashing, and long-running JavaScript operations.
- Xcode Instruments: For iOS-specific profiling, use the Time Profiler and Allocations instruments. These show native-level performance data that JavaScript tools simply can't capture — including Core Animation rendering, memory allocations, and energy impact.
- Android Studio Profiler: For Android, the built-in profiler provides CPU, memory, network, and energy profiling. The CPU profiler is particularly useful for identifying native thread contention.
Automated Performance Testing
The most effective performance strategy is catching regressions before they ship. Integrate performance benchmarks into your CI pipeline:
// performance.test.js — Using Reassure for performance regression testing
import { measurePerformance } from 'reassure';
import { ProductList } from '../components/ProductList';
test('ProductList renders 1000 items efficiently', async () => {
const products = generateMockProducts(1000);
await measurePerformance(
<ProductList products={products} />,
{ runs: 10 }
);
});
test('ProductCard does not re-render without prop changes', async () => {
const product = generateMockProduct();
await measurePerformance(
<ProductCard product={product} />,
{
runs: 10,
scenario: async (screen) => {
// Trigger parent re-render without changing this component's props
await screen.rerender(<ProductCard product={product} />);
},
}
);
});
Reassure, built by Callstack, runs render performance tests and compares results against your baseline. If a component's render time regresses beyond a configurable threshold, CI fails. It's a game-changer for teams that care about shipping fast apps consistently.
Startup Optimization: First Impressions Matter
App startup time is the first performance metric your users experience. A slow launch creates an immediate negative impression, regardless of how smooth the app runs afterward. You don't get a second chance at a first impression (cliche but true).
Measuring Startup Time
Define and track specific startup milestones:
// Track startup milestones
const startupMetrics = {
nativeInit: Date.now(), // Set in native code
jsInit: 0,
firstRender: 0,
interactive: 0,
};
// In your root component
function App() {
useEffect(() => {
startupMetrics.firstRender = Date.now();
// Mark as interactive when initial data is loaded
InteractionManager.runAfterInteractions(() => {
startupMetrics.interactive = Date.now();
const tti = startupMetrics.interactive - startupMetrics.nativeInit;
console.log(`Time to Interactive: ${tti}ms`);
});
}, []);
}
Proven Startup Optimizations
- Defer non-critical work: Use
InteractionManager.runAfterInteractionsto schedule analytics initialization, background syncs, and other non-visible work after the first screen is interactive. - Optimize your splash screen: Use
expo-splash-screento keep the native splash screen visible until your app is truly ready. This hides the JavaScript initialization time behind a polished native experience. - Minimize root component complexity: Your root component and the first visible screen should import as few dependencies as possible. Heavy screens deeper in the navigation stack should be lazy-loaded.
- Preload critical assets: Use
Asset.loadAsyncandFont.loadAsyncduring the splash screen phase to prevent layout shifts after the app renders.
import * as SplashScreen from 'expo-splash-screen';
import * as Font from 'expo-font';
import { Asset } from 'expo-asset';
// Keep splash screen visible during preparation
SplashScreen.preventAutoHideAsync();
function App() {
const [isReady, setIsReady] = useState(false);
useEffect(() => {
async function prepare() {
try {
// Load critical assets in parallel
await Promise.all([
Font.loadAsync({
'Inter-Regular': require('./assets/fonts/Inter-Regular.ttf'),
'Inter-Bold': require('./assets/fonts/Inter-Bold.ttf'),
}),
Asset.loadAsync([
require('./assets/images/logo.png'),
]),
]);
} catch (e) {
console.warn('Asset loading error:', e);
} finally {
setIsReady(true);
}
}
prepare();
}, []);
useEffect(() => {
if (isReady) {
SplashScreen.hideAsync();
}
}, [isReady]);
if (!isReady) return null;
return <AppNavigator />;
}
Performance Checklist: Putting It All Together
Here's a prioritized checklist for optimizing your React Native app in 2026. Start from the top — each step builds on the previous one:
- Enable the New Architecture — The single biggest performance improvement. Verify all dependencies are compatible.
- Confirm Hermes is active — Make sure you're running Hermes (and consider opting into Hermes V1 on RN 0.82+).
- Enable React Compiler — Automatic memoization with zero code changes. Available by default in Expo SDK 54+.
- Audit your bundle — Visualize your bundle, remove unused dependencies, replace heavy libraries with lighter alternatives.
- Optimize lists — Migrate from FlatList to FlashList or Legend List for any list over 50 items.
- Optimize images — Use
expo-imagewith WebP format, appropriate sizing, blurhash placeholders, and prefetching. - Move animations to UI thread — Use Reanimated for all animations. Eliminate any
AnimatedAPI usage that runs on the JS thread. - Isolate frequently-updating state — Extract timers, counters, and real-time data into leaf components.
- Clean up subscriptions — Audit every
useEffectfor proper cleanup. Use the memory monitor hook to catch leaks early. - Set up performance testing — Add Reassure tests for critical components. Catch regressions in CI before they reach users.
Wrapping Up
React Native performance optimization in 2026 is both easier and more impactful than it's ever been. The New Architecture, Hermes V1, and the React Compiler handle a lot of the heavy lifting automatically — but they're not magic. The best-performing apps combine these platform-level improvements with thoughtful component architecture, efficient data handling, and regular measurement.
Start with the architecture-level wins (New Architecture, Hermes, React Compiler), then work your way down to component-level optimizations like lists, images, and animations. Measure before and after every change with the profiling tools we covered. And most importantly, set up automated performance testing so that once you achieve great performance, it stays great as your app evolves.
The tools and techniques in this guide represent the current state of the art for React Native performance. As Hermes V1 matures with JIT compilation and Static Hermes features, and as the React Compiler continues to improve, the performance ceiling will only keep rising. Build your optimization practices now, and you'll be well-positioned to take advantage of every improvement that comes next.