Introduction
A slow app frustrates users and leads to abandonment. React Native powers major applications like Instagram, Facebook, and Shopify — but achieving smooth performance requires deliberate optimization. In 2026, React Native’s new architecture (Fabric renderer, TurboModules, JSI, and Codegen) has become the default, delivering significant performance improvements over the legacy bridge-based architecture. This guide covers the performance landscape, profiling tools, optimization techniques, and best practices for building fast, smooth React Native applications.
Understanding React Native Performance
The Performance Pillars
React Native applications operate across multiple threads, each of which can become a bottleneck:
| Thread | Role | Impact When Blocked |
|---|---|---|
| JavaScript (JS) | Business logic, state updates, event handling | UI jank, slow navigation |
| Main (UI) | Native rendering, layout, animations | Stutter, dropped frames |
| Shadow | Layout calculation (Yoga) | Delayed layout |
| Native Modules | I/O, network, native APIs | Blocked JS thread |
| Hermes Compiler | Pre-compilation to bytecode | Faster startup |
flowchart TD
JS[JavaScript Thread] --> JSI[JSI Layer]
JSI --> TM[TurboModules]
JSI --> F[Fabric Renderer]
F --> UI[Main/UI Thread]
TM --> NM[Native Modules]
JS --> ST[Shadow Thread]
ST --> UI
style JS fill:#fff3e0
style UI fill:#e1f5fe
style F fill:#f3e5f5
style TM fill:#e8f5e9
The New Architecture (React Native 0.76+)
React Native’s new architecture, stable since 0.76, replaces the legacy bridge with a direct JSI (JavaScript Interface) binding:
| Component | Legacy (Bridge) | New Architecture |
|---|---|---|
| Communication | Async serialized JSON bridge | Synchronous JSI calls |
| Native Modules | Bridge-based, async only | TurboModules, sync optional |
| Renderer | Paper (old) | Fabric (new) |
| Codegen | Manual type definitions | Auto-generated by Codegen |
| Startup | Parse all JS at runtime | Hermes bytecode pre-compilation |
The new architecture eliminates the serialization overhead of the bridge. JSI allows JavaScript to hold references to native objects and call them synchronously, dramatically reducing the latency of JS-to-native communication.
Hermes Engine
Hermes is a JavaScript engine optimized for React Native. It pre-compiles JavaScript to bytecode during build time, reducing startup time and memory usage.
Enabling Hermes
Configure Hermes in your app configuration:
{
"expo": {
"jsEngine": "hermes",
"hermes": {
"compression": "brotli"
}
}
}
// For bare React Native - app.json
{
"name": "MyApp",
"version": "1.0.0",
"react-native": {
"jsEngine": "hermes"
}
}
// android/app/build.gradle
project.ext.react = [
enableHermes: true,
]
// ios/Podfile
:hermes_enabled => true
Verifying Hermes
Check that Hermes is running at runtime:
import { NativeModules } from 'react-native';
const { HermesEngine } = NativeModules;
function checkEngine() {
if (HermesEngine) {
console.log('Hermes enabled:', HermesEngine.isEnabled?.());
} else {
console.log('Hermes not detected');
}
}
Hermes-Specific Optimizations
// Hermes supports modern JS but has some differences
// ✅ Supported - Nullish coalescing
const value = input ?? defaultValue;
// ✅ Supported - Optional chaining
const name = user?.profile?.name;
// ✅ Supported - BigInt
const big = BigInt(9007199254740991);
// ❌ Avoid - Proxy (not supported in Hermes)
// const proxy = new Proxy(target, handler);
// ❌ Avoid - Symbol.toStringTag
// class MyClass { get [Symbol.toStringTag]() { return 'MyClass'; } }
// ✅ Use - JSON.parse/stringify (Hermes has optimized implementation)
const data = JSON.parse(jsonString);
// ✅ Use - Array methods (optimized in Hermes)
const filtered = items.filter(item => item.active).map(item => item.name);
Hermes Performance Metrics
| Metric | Without Hermes | With Hermes | Improvement |
|---|---|---|---|
| App startup (cold) | ~1200ms | ~600ms | 50% faster |
| App startup (warm) | ~400ms | ~200ms | 50% faster |
| JS bundle size | ~25 MB | ~8 MB (bytecode) | 68% smaller |
| Peak memory usage | ~120 MB | ~85 MB | 29% less |
| Time to interactive | ~1800ms | ~900ms | 50% faster |
JSI (JavaScript Interface)
JSI is a lightweight C++ API that enables JavaScript to hold references to native objects and call them synchronously. Unlike the legacy bridge, which serialized messages to JSON and sent them asynchronously, JSI provides direct function calls with zero serialization overhead.
How JSI Works
sequenceDiagram
participant JS as JavaScript
participant JSI as JSI Layer
participant NM as Native Module
JS->>JSI: Call nativeFunction(args)
Note over JSI: Direct C++ function call
JSI->>NM: Execute native code
NM-->>JSI: Return value
JSI-->>JS: Return value
Note over JS,NM: No JSON serialization
Note over JS,JSI: Synchronous execution
TurboModules
TurboModules leverage JSI to provide lazy-loaded, synchronous native module access:
// C++ TurboModule spec for CalendarModule
#include <react/renderer/components/MyModuleSpec/MyModuleSpecJSI.h>
class CalendarModuleSpecJSI : public JObjectWrapper {
public:
static void install(jsi::Runtime &runtime) {
auto module = std::make_shared<CalendarModule>();
jsi::Object moduleObj(runtime);
moduleObj.setProperty(
runtime,
"createEvent",
jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "createEvent"),
1,
[module](jsi::Runtime &rt,
const jsi::Value &thisVal,
const jsi::Value *args,
size_t count) -> jsi::Value {
std::string title = args[0].asString(rt).utf8(rt);
auto result = module->createEvent(title);
return jsi::String::createFromUtf8(rt, result);
}
)
);
runtime.global().setProperty(runtime, "CalendarModule", moduleObj);
}
};
// JavaScript usage - direct synchronous call
const { CalendarModule } = NativeModules;
// With TurboModules, this becomes synchronous
const eventId = CalendarModule.createEvent('Meeting');
console.log('Event created:', eventId);
Auto-Generated Specs with Codegen
Codegen generates type-safe native module interfaces automatically:
// CalendarModule.ts — spec file
import { TurboModule, TurboModuleRegistry } from 'react-native';
import type { Double } from 'react-native/Libraries/Types/CodegenTypes';
export interface Spec extends TurboModule {
createEvent(title: string, date: Double): string;
updateEvent(id: string, title: string): boolean;
deleteEvent(id: string): void;
}
export default TurboModuleRegistry.getEnforcing<Spec>('CalendarModule');
Fabric Renderer
Fabric is the new rendering system that replaces the legacy Paper renderer. It runs synchronously on the UI thread and supports features like view flattening and event prioritization.
Fabric Benefits
| Feature | Paper (Legacy) | Fabric (New) |
|---|---|---|
| Rendering | Async shadow tree + commit | Synchronous, atomic commits |
| View flattening | Manual | Automatic |
| Events | Async bubbles | Priority-based dispatch |
| State updates | Batch on frame | Immediate or deferred |
| Interop | None | Mixed Paper/Fabric views |
View Flattening
Fabric automatically flattens unnecessary view hierarchies:
// ❌ Legacy - Creates many native views
function DeeplyNestedView() {
return (
<View style={styles.outer}>
<View style={styles.middle}>
<View style={styles.inner}>
<Text>Hello</Text>
</View>
</View>
</View>
);
}
// ✅ Fabric - View flattening merges simple containers
function OptimizedNestedView() {
return (
<View style={[styles.outer, styles.middle, styles.inner]}>
<Text>Hello</Text>
</View>
);
}
Event Prioritization
Fabric supports event priority levels to ensure critical interactions remain responsive:
import { TouchableOpacity, Pressable } from 'react-native';
// Fabric gives press events higher priority than scroll events
<Pressable
onPress={(event) => {
// This event gets highest priority
handlePress(event);
}}
onPressIn={() => {
// Immediate visual feedback at max priority
setPressed(true);
}}
style={({ pressed }) => [
styles.button,
pressed && styles.buttonPressed,
]}
/>
Bundle Size Optimization
Reducing bundle size improves initial load time and memory usage.
Metro Bundler Configuration
// metro.config.js
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
const config = {
transformer: {
// Enable tree-shaking
minifierConfig: {
compress: {
drop_console: true, // Remove console.log in production
drop_debugger: true,
passes: 2,
},
mangle: {
safari10: false,
},
},
},
resolver: {
// Exclude unnecessary modules
blockList: [/node_modules\/.*\/__tests__/],
sourceExts: ['js', 'jsx', 'ts', 'tsx', 'mjs'],
},
// Parallel bundling for faster builds
maxWorkers: 4,
};
module.exports = mergeConfig(getDefaultConfig(__dirname), config);
Tree Shaking and Dead Code Elimination
// ❌ Bad - Imports entire library
import { isNil, isEmpty, debounce } from 'lodash';
// ✅ Good - Imports only needed functions
import isNil from 'lodash/isNil';
import isEmpty from 'lodash/isEmpty';
import debounce from 'lodash/debounce';
Bundle Analysis
# Analyze bundle composition
npx react-native bundle \
--platform android \
--dev false \
--entry-file index.js \
--bundle-output /tmp/bundle.js \
--assets-dest /tmp/assets
# Visualize bundle modules
npx source-map-explorer /tmp/bundle.js
Image Optimization
Images are often the largest contributor to app size and memory usage.
Image Libraries
// expo-image (preferred for Expo)
import { Image } from 'expo-image';
<Image
source="https://example.com/large-photo.jpg"
style={{ width: 200, height: 200 }}
contentFit="cover"
cachePolicy="memory-disk"
transition={300}
placeholder={{ blurhash: 'LKO2?U%2Tw=w]~RBVZRi};RPxuwH' }}
/>
// expo-image provides:
// - Automatic WebP/AVIF conversion
// - Memory and disk caching
// - Blurhash placeholders
// - Smooth transitions
// - Reduced memory usage vs React Native Image
// react-native-fast-image (bare RN alternative)
import FastImage from 'react-native-fast-image';
<FastImage
style={{ width: 200, height: 200 }}
source={{
uri: 'https://example.com/image.jpg',
headers: { Authorization: 'Bearer token' },
priority: FastImage.priority.normal,
cache: FastImage.cacheControl.immutable,
}}
resizeMode={FastImage.resizeMode.cover}
/>
Image Optimization Techniques
// Prefetch critical images
import { Image } from 'expo-image';
const criticalImages = [
'https://example.com/hero.jpg',
'https://example.com/logo.png',
];
useEffect(() => {
Image.prefetch(criticalImages);
}, []);
// Responsive images based on screen size
function ResponsiveImage({ uri, width }: { uri: string; width: number }) {
const screenWidth = Dimensions.get('window').width;
const imageWidth = Math.min(width, screenWidth * 2); // 2x for retina
return (
<Image
source={`${uri}?w=${imageWidth}&q=80`}
style={{ width, height: width * 0.75 }}
contentFit="cover"
/>
);
}
// Thumbnail generation for lists
function ImageThumbnail({ uri }: { uri: string }) {
return (
<Image
source={`${uri}?w=100&q=60`} // Low-res thumbnail
style={{ width: 100, height: 100 }}
contentFit="cover"
/>
);
}
List Optimization
Lists are the most common performance bottleneck in React Native apps.
FlatList Configuration
// Fully optimized FlatList
import { FlatList } from 'react-native';
const ITEM_HEIGHT = 120;
const LIST_PADDING = 16;
<FlatList
data={items}
renderItem={renderItem}
// Performance optimizations
keyExtractor={useCallback((item) => item.id, [])}
getItemLayout={useCallback(
(_, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
}),
[]
)}
// Rendering constraints
removeClippedSubviews={true}
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={5}
updateCellsBatchingPeriod={50}
// Memory optimization
maxToRenderPerBatch={10}
windowSize={5}
// Prevent jank on slow scroll
disableScrollViewPanResponder={false}
// Headers and footers
ListHeaderComponent={useCallback(
() => <ListHeader />,
[]
)}
ListFooterComponent={useCallback(
() => <ListFooter />,
[]
)}
// Separator optimization
ItemSeparatorComponent={useCallback(
() => <View style={styles.separator} />,
[]
)}
// Empty state
ListEmptyComponent={useCallback(
() => <EmptyState />,
[]
)}
contentContainerStyle={styles.listContent}
/>
FlashList (Shopify)
For extremely large lists, FlashList outperforms FlatList by recycling views more aggressively:
import { FlashList } from '@shopify/flash-list';
function LargeList({ items }) {
return (
<FlashList
data={items}
renderItem={({ item }) => <ListItem item={item} />}
estimatedItemSize={120}
estimatedListSize={{
width: Dimensions.get('window').width,
height: Dimensions.get('window').height,
}}
keyExtractor={(item) => item.id}
overrideItemLayout={(layout, item) => {
// Dynamic item sizing
layout.size = item.isExpanded ? 200 : 80;
}}
// FlashList handles these automatically:
// - removeClippedSubviews
// - windowSize optimization
// - Recycling pool management
/>
);
}
| Feature | FlatList | FlashList |
|---|---|---|
| Memory usage | Higher | ~70% less |
| Scrolling smoothness | Good for <1000 items | Excellent for 100k+ |
| Auto-sizing | Manual getItemLayout | Automatic (estimatedItemSize) |
| View recycling | Yes | Yes, with smarter pool |
| Content masking | No | Auto-masks offscreen content |
| Setup complexity | Medium | Low |
Memoization and Re-render Prevention
React.memo
Prevent unnecessary re-renders of expensive components:
import { memo } from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
// ✅ Memoize list items
interface ListItemProps {
item: Item;
onPress: (id: string) => void;
}
const ListItem = memo(function ListItem({ item, onPress }: ListItemProps) {
return (
<TouchableOpacity
onPress={() => onPress(item.id)}
style={styles.item}
>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.subtitle}>{item.description}</Text>
</TouchableOpacity>
);
}, (prevProps, nextProps) => {
// Custom comparison to avoid deep equality checks
return (
prevProps.item.id === nextProps.item.id &&
prevProps.item.title === nextProps.item.title &&
prevProps.item.updatedAt === nextProps.item.updatedAt
);
});
useCallback and useMemo
Stabilize function references and memoize expensive computations:
import { useCallback, useMemo, useState } from 'react';
function ProductList({ products, filter }: ProductListProps) {
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc');
// ✅ Stable callback - same reference unless dependencies change
const handlePress = useCallback((productId: string) => {
navigation.navigate('ProductDetail', { productId });
}, [navigation]);
// ✅ Memoized expensive computation
const filteredAndSortedProducts = useMemo(() => {
console.log('Computing filtered products...');
let result = products;
if (filter.category) {
result = result.filter(p => p.category === filter.category);
}
if (filter.minPrice) {
result = result.filter(p => p.price >= filter.minPrice);
}
return result.sort((a, b) =>
sortOrder === 'asc'
? a.price - b.price
: b.price - a.price
);
}, [products, filter.category, filter.minPrice, sortOrder]);
// ✅ Memoized style object
const containerStyle = useMemo(() => ({
paddingHorizontal: 16,
paddingTop: Platform.OS === 'ios' ? 44 : 0,
}), []);
return (
<View style={containerStyle}>
<FlatList
data={filteredAndSortedProducts}
renderItem={({ item }) => (
<ListItem item={item} onPress={handlePress} />
)}
keyExtractor={useCallback((item) => item.id, [])}
/>
</View>
);
}
Context Optimization
Avoid re-rendering entire subtrees when context values change:
import { createContext, useContext, useMemo, useState, memo } from 'react';
// Split context into smaller pieces
const AuthContext = createContext<AuthContextType | null>(null);
const ThemeContext = createContext<ThemeContextType | null>(null);
function AppProvider({ children }: { children: React.ReactNode }) {
const [authState, setAuthState] = useState<AuthState>(initialAuth);
const [theme, setTheme] = useState<Theme>('light');
const authValue = useMemo(
() => ({ state: authState, setAuthState }),
[authState]
);
const themeValue = useMemo(
() => ({ theme, setTheme, toggleTheme }),
[theme]
);
return (
<AuthContext.Provider value={authValue}>
<ThemeContext.Provider value={themeValue}>
{children}
</ThemeContext.Provider>
</AuthContext.Provider>
);
}
// ✅ Only re-renders when auth changes, not theme
const ProfileScreen = memo(function ProfileScreen() {
const { state } = useContext(AuthContext)!;
return <Text>{state.user?.name}</Text>;
});
Animation Performance
Using Reanimated
React Native Reanimated runs animations on the UI thread for 60 FPS performance:
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withTiming,
interpolate,
Extrapolate,
} from 'react-native-reanimated';
function AnimatedCard() {
const scale = useSharedValue(1);
const opacity = useSharedValue(0);
const rotation = useSharedValue(0);
// Layout animation
useEffect(() => {
opacity.value = withTiming(1, { duration: 300 });
rotation.value = withSpring(0, { damping: 15 });
}, []);
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ scale: scale.value },
{ rotateZ: `${rotation.value}deg` },
],
opacity: opacity.value,
}));
const onPressIn = () => {
scale.value = withSpring(0.95, { damping: 20 });
};
const onPressOut = () => {
scale.value = withSpring(1, { damping: 15 });
};
return (
<Animated.View style={[styles.card, animatedStyle]}>
<Animated.Image
source={{ uri: 'https://example.com/image.jpg' }}
sharedTransitionTag="shared-image"
style={styles.image}
/>
</Animated.View>
);
}
Gesture Handler
Use react-native-gesture-handler for native-thread gesture processing:
import { GestureDetector, Gesture } from 'react-native-gesture-handler';
import Animated from 'react-native-reanimated';
function SwipeableRow({ children, onDelete }) {
const translateX = useSharedValue(0);
const isSwiped = useSharedValue(false);
const panGesture = Gesture.Pan()
.onUpdate((event) => {
translateX.value = Math.max(-100, Math.min(0, event.translationX));
})
.onEnd(() => {
if (translateX.value < -50) {
translateX.value = withSpring(-100);
isSwiped.value = true;
} else {
translateX.value = withSpring(0);
isSwiped.value = false;
}
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
return (
<View>
{/* Delete action behind the row */}
<View style={styles.deleteContainer}>
<TouchableOpacity onPress={onDelete}>
<Text style={styles.deleteText}>Delete</Text>
</TouchableOpacity>
</View>
<GestureDetector gesture={panGesture}>
<Animated.View style={animatedStyle}>
{children}
</Animated.View>
</GestureDetector>
</View>
);
}
Performance Monitoring
Profiling Tools
Use React Native’s built-in profiler and third-party tools:
# React Native Performance Monitor (Dev Menu)
# Shake device → Show Perf Monitor
# Displays: FPS, JS FPS, RAM usage
# Systrace for Android
npx react-native start --reset-cache
# Then in another terminal:
# Open dev menu → Profile Hermes → record system trace
# Instruments for iOS
# Xcode → Product → Profile → Time Profiler
Performance Monitor API
import { PerformanceObserver, performance } from 'react-native-performance';
// Track custom metrics
const mark = performance.mark('list-render');
// ... render list ...
performance.measure('list-render-time', 'list-render');
// Observe performance entries
const observer = new PerformanceObserver((list, obs) => {
const entries = list.getEntries();
entries.forEach(entry => {
if (entry.duration > 16) { // Over 1 frame at 60fps
console.warn('Slow operation:', entry.name, entry.duration);
}
});
});
observer.observe({ entryTypes: ['measure'] });
Custom Performance Tracking
import { InteractionManager } from 'react-native';
// Measure time to interactive
function useTTIMeasurement(screenName: string) {
const startTime = useRef(performance.now());
useEffect(() => {
InteractionManager.runAfterInteractions(() => {
const tti = performance.now() - startTime.current;
analytics.track('TTI', {
screen: screenName,
duration: tti,
});
if (tti > 2000) {
console.warn(`Slow TTI for ${screenName}: ${tti}ms`);
}
});
}, []);
}
// Frame rate monitoring
function useFrameRateMonitor() {
const frameDurations = useRef<number[]>([]);
const lastFrame = useRef(performance.now());
useEffect(() => {
const interval = setInterval(() => {
const now = performance.now();
const delta = now - lastFrame.current;
frameDurations.current.push(delta);
lastFrame.current = now;
// Keep last 60 samples
if (frameDurations.current.length > 60) {
const avg = frameDurations.current.reduce((a, b) => a + b) / 60;
const fps = 1000 / avg;
if (fps < 30) {
console.warn('Low frame rate:', fps.toFixed(1), 'FPS');
}
frameDurations.current = [];
}
}, 1000);
return () => clearInterval(interval);
}, []);
}
Startup Time Optimization
Lazy Loading Screens
Load screens only when needed:
import { lazy, Suspense, ComponentType } from 'react';
import { ActivityIndicator, View, Text } from 'react-native';
// Define lazy-loaded screens
const ProfileScreen = lazy(() => import('./screens/ProfileScreen'));
const SettingsScreen = lazy(() => import('./screens/SettingsScreen'));
const DashboardScreen = lazy(() => import('./screens/DashboardScreen'));
const LAZY_SCREENS = {
Profile: ProfileScreen,
Settings: SettingsScreen,
Dashboard: DashboardScreen,
};
function LazyScreen({ route }: { route: { name: keyof typeof LAZY_SCREENS } }) {
const Screen = LAZY_SCREENS[route.name] as ComponentType<any>;
return (
<Suspense fallback={<ScreenFallback />}>
<Screen />
</Suspense>
);
}
function ScreenFallback() {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" color="#007AFF" />
<Text style={styles.loadingText}>Loading...</Text>
</View>
);
}
Code Splitting with Metro
// metro.config.js - Configure code splitting
const config = {
transformer: {
// Enable inline requires for lazy imports
inlineRequires: {
blockList: {
// Don't inline requires for these modules
'react-native': true,
'react': true,
},
},
// Enable the new inline requires transform
experimentalImportSupport: true,
},
};
Common Performance Pitfalls
| Issue | Cause | Solution |
|---|---|---|
| Unnecessary re-renders | Inline functions in render | useCallback, React.memo |
| Large bundle size | Importing entire libraries | Tree-shaking, code splitting |
| Memory leaks | Unsubscribed listeners | useEffect cleanup return |
| Slow list scrolling | No getItemLayout | getItemLayout or FlashList |
| Frequent state updates | Unoptimized context | Split contexts, useMemo |
| Heavy animations on JS thread | Using Animated API | Reanimated for UI-thread animations |
| Large images in lists | Full-res images in render | Thumbnails, expo-image caching |
| Bridge serialization overhead | Legacy architecture | Upgrade to Fabric/TurboModules |
Performance Checklist
Before shipping, verify these performance items:
- Hermes engine enabled and verified
- New architecture (Fabric + TurboModules) enabled
- FlatList/FlashList configured with keyExtractor and getItemLayout
- Components memoized with React.memo where beneficial
- useCallback/useMemo applied to callbacks and computations
- Images use expo-image or fast-image with caching
- Bundle size analyzed and optimized
- Console.log removed in production
- Animations use Reanimated (UI thread)
- Memory leaks checked with React DevTools
- FPS maintains 55+ during scrolling
- Startup time under 2 seconds
- Frame drops profiled with Systrace/Instruments
Conclusion
React Native performance optimization requires understanding the platform’s threading model and choosing the right tools for each bottleneck. The new architecture (Fabric, TurboModules, JSI) eliminates the legacy bridge overhead, while Hermes provides bytecode pre-compilation for faster startup. List virtualization, image optimization, and proper memoization address the most common runtime performance issues.
The key to good performance is measurement — profile before and after optimizations to ensure changes are actually effective. Use React Native’s built-in Performance Monitor, Systrace, and Instruments to identify bottlenecks. With the techniques in this guide, React Native applications can achieve smooth 60 FPS performance even with complex UIs and large datasets.
Resources
- React Native Performance Documentation
- Hermes Engine Documentation
- React Native New Architecture
- Shopify FlashList
- React Native Reanimated
- React Native Gesture Handler
- Expo Image Documentation
- Metro Bundler Configuration
- Fabric Renderer Deep Dive
Comments