Back to Journal
Mobile/Frontend

How to Build React Native Performance Using React

Step-by-step tutorial for building React Native Performance with React, from project setup through deployment.

Muneer Puthiya Purayil 16 min read

React Native performance optimization requires understanding how React's rendering model interacts with the native platform. This tutorial builds a performance-optimized React Native application from scratch, implementing each optimization technique with measurable before-and-after results.

Project Setup

bash
1npx react-native init PerformanceApp --template react-native-template-typescript
2cd PerformanceApp
3npm install @shopify/flash-list react-native-fast-image react-native-reanimated react-native-gesture-handler react-native-screens @tanstack/react-query zustand
4cd ios && pod install && cd ..
5 

Enable performance foundations in android/gradle.properties:

properties
hermesEnabled=true newArchEnabled=true

Measuring Baseline Performance

Before optimizing, establish baselines:

typescript
1// src/utils/performance.ts
2import { PerformanceObserver, performance } from 'react-native-performance';
3 
4export function measureStartup(): void {
5 performance.mark('app_start');
6 
7 // Mark when first screen is interactive
8 // Call this in your root component's useEffect
9}
10 
11export function markInteractive(): void {
12 performance.mark('app_interactive');
13 performance.measure('startup', 'app_start', 'app_interactive');
14 const [measure] = performance.getEntriesByName('startup');
15 console.log(`Time to Interactive: ${measure.duration.toFixed(0)}ms`);
16}
17 
18// FPS monitor
19export function createFPSMonitor() {
20 let frameCount = 0;
21 let lastTime = Date.now();
22 
23 return {
24 onFrame() {
25 frameCount++;
26 const now = Date.now();
27 if (now - lastTime >= 1000) {
28 const fps = Math.round((frameCount * 1000) / (now - lastTime));
29 if (fps < 55) {
30 console.warn(`Low FPS: ${fps}`);
31 }
32 frameCount = 0;
33 lastTime = now;
34 }
35 },
36 };
37}
38 

Building the Data Layer

Optimized API Client

typescript
1// src/api/client.ts
2import { QueryClient } from '@tanstack/react-query';
3 
4export const queryClient = new QueryClient({
5 defaultOptions: {
6 queries: {
7 staleTime: 5 * 60 * 1000,
8 gcTime: 30 * 60 * 1000,
9 retry: 2,
10 refetchOnWindowFocus: false,
11 refetchOnReconnect: true,
12 },
13 },
14});
15 
16// Type-safe API functions
17interface PaginatedResponse<T> {
18 data: T[];
19 nextCursor: string | null;
20 total: number;
21}
22 
23async function fetchJSON<T>(url: string): Promise<T> {
24 const response = await fetch(url, {
25 headers: { 'Content-Type': 'application/json' },
26 });
27 if (!response.ok) throw new Error(`HTTP ${response.status}`);
28 return response.json();
29}
30 
31export const api = {
32 getProducts: (cursor?: string) =>
33 fetchJSON<PaginatedResponse<Product>>(`/api/products?cursor=${cursor ?? ''}`),
34 getProduct: (id: string) =>
35 fetchJSON<Product>(`/api/products/${id}`),
36};
37 

Query Hooks with Pre-computation

typescript
1// src/hooks/useProducts.ts
2import { useInfiniteQuery } from '@tanstack/react-query';
3 
4// Pre-compute formatted values outside render
5function enrichProduct(product: Product): EnrichedProduct {
6 return {
7 ...product,
8 formattedPrice: `$${product.price.toFixed(2)}`,
9 formattedRating: `${product.rating.toFixed(1)} ★`,
10 thumbnailUrl: `${product.imageUrl}?w=200&h=200&fit=cover`,
11 };
12}
13 
14export function useProducts() {
15 return useInfiniteQuery({
16 queryKey: ['products'],
17 queryFn: ({ pageParam }) => api.getProducts(pageParam),
18 getNextPageParam: (last) => last.nextCursor,
19 select: (data) => ({
20 ...data,
21 pages: data.pages.map((page) => ({
22 ...page,
23 data: page.data.map(enrichProduct),
24 })),
25 }),
26 });
27}
28 

Building the List Screen

Optimized Product List

typescript
1// src/screens/ProductList.tsx
2import { FlashList, ListRenderItem } from '@shopify/flash-list';
3import FastImage from 'react-native-fast-image';
4 
5const ProductCard = React.memo<{ product: EnrichedProduct; onPress: (id: string) => void }>(
6 ({ product, onPress }) => {
7 const handlePress = useCallback(() => onPress(product.id), [product.id, onPress]);
8 
9 return (
10 <Pressable onPress={handlePress} style={styles.card}>
11 <FastImage
12 source={{ uri: product.thumbnailUrl, priority: FastImage.priority.normal }}
13 style={styles.productImage}
14 resizeMode={FastImage.resizeMode.cover}
15 />
16 <View style={styles.productInfo}>
17 <Text style={styles.productName} numberOfLines={2}>
18 {product.name}
19 </Text>
20 <Text style={styles.productPrice}>{product.formattedPrice}</Text>
21 <Text style={styles.productRating}>{product.formattedRating}</Text>
22 </View>
23 </Pressable>
24 );
25 },
26);
27 
28export function ProductListScreen() {
29 const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useProducts();
30 const navigation = useNavigation();
31 
32 const products = useMemo(
33 () => data?.pages.flatMap((p) => p.data) ?? [],
34 [data],
35 );
36 
37 const handlePress = useCallback(
38 (id: string) => navigation.navigate('ProductDetail', { id }),
39 [navigation],
40 );
41 
42 const renderItem: ListRenderItem<EnrichedProduct> = useCallback(
43 ({ item }) => <ProductCard product={item} onPress={handlePress} />,
44 [handlePress],
45 );
46 
47 return (
48 <FlashList
49 data={products}
50 renderItem={renderItem}
51 estimatedItemSize={120}
52 keyExtractor={keyExtractor}
53 numColumns={2}
54 onEndReached={() => hasNextPage && fetchNextPage()}
55 onEndReachedThreshold={0.5}
56 ListFooterComponent={isFetchingNextPage ? <LoadingFooter /> : null}
57 />
58 );
59}
60 
61const keyExtractor = (item: EnrichedProduct) => item.id;
62 

Need a second opinion on your mobile/frontend architecture?

I run free 30-minute strategy calls for engineering teams tackling this exact problem.

Book a Free Call

Building Animated Interactions

Pull-to-Refresh with Reanimated

typescript
1import Animated, {
2 useSharedValue,
3 useAnimatedStyle,
4 withSpring,
5 runOnJS,
6 useAnimatedScrollHandler,
7} from 'react-native-reanimated';
8 
9function AnimatedRefreshControl({
10 onRefresh,
11 refreshing,
12}: {
13 onRefresh: () => void;
14 refreshing: boolean;
15}) {
16 const pullDistance = useSharedValue(0);
17 const threshold = 80;
18 
19 const scrollHandler = useAnimatedScrollHandler({
20 onScroll: (event) => {
21 if (event.contentOffset.y < 0) {
22 pullDistance.value = Math.abs(event.contentOffset.y);
23 }
24 },
25 onEndDrag: () => {
26 if (pullDistance.value > threshold) {
27 runOnJS(onRefresh)();
28 }
29 pullDistance.value = withSpring(0);
30 },
31 });
32 
33 const indicatorStyle = useAnimatedStyle(() => ({
34 height: Math.min(pullDistance.value, threshold),
35 opacity: Math.min(pullDistance.value / threshold, 1),
36 }));
37 
38 return { scrollHandler, indicatorStyle };
39}
40 

Swipeable Delete

typescript
1import { Gesture, GestureDetector } from 'react-native-gesture-handler';
2import Animated, {
3 useSharedValue,
4 useAnimatedStyle,
5 withTiming,
6 withSpring,
7 runOnJS,
8} from 'react-native-reanimated';
9 
10function SwipeToDelete({
11 children,
12 onDelete,
13}: {
14 children: React.ReactNode;
15 onDelete: () => void;
16}) {
17 const translateX = useSharedValue(0);
18 
19 const gesture = Gesture.Pan()
20 .activeOffsetX([-10, 10])
21 .onUpdate((e) => {
22 translateX.value = Math.min(0, e.translationX);
23 })
24 .onEnd(() => {
25 if (translateX.value < -120) {
26 translateX.value = withTiming(-400, { duration: 200 });
27 runOnJS(onDelete)();
28 } else {
29 translateX.value = withSpring(0);
30 }
31 });
32 
33 const rowStyle = useAnimatedStyle(() => ({
34 transform: [{ translateX: translateX.value }],
35 }));
36 
37 const deleteStyle = useAnimatedStyle(() => ({
38 width: Math.abs(Math.min(translateX.value, 0)),
39 opacity: Math.min(Math.abs(translateX.value) / 120, 1),
40 }));
41 
42 return (
43 <View style={styles.swipeContainer}>
44 <Animated.View style={[styles.deleteButton, deleteStyle]}>
45 <Text style={styles.deleteText}>Delete</Text>
46 </Animated.View>
47 <GestureDetector gesture={gesture}>
48 <Animated.View style={rowStyle}>{children}</Animated.View>
49 </GestureDetector>
50 </View>
51 );
52}
53 

State Management with Zustand

typescript
1// src/store/app.ts
2import { create } from 'zustand';
3 
4interface AppState {
5 cart: CartItem[];
6 addToCart: (product: Product) => void;
7 removeFromCart: (productId: string) => void;
8 cartTotal: () => number;
9}
10 
11export const useAppStore = create<AppState>((set, get) => ({
12 cart: [],
13 addToCart: (product) =>
14 set((state) => ({
15 cart: [...state.cart, { ...product, quantity: 1 }],
16 })),
17 removeFromCart: (productId) =>
18 set((state) => ({
19 cart: state.cart.filter((item) => item.id !== productId),
20 })),
21 cartTotal: () =>
22 get().cart.reduce((sum, item) => sum + item.price * item.quantity, 0),
23}));
24 
25// Granular selectors prevent unnecessary re-renders
26function CartBadge() {
27 const itemCount = useAppStore((s) => s.cart.length);
28 if (itemCount === 0) return null;
29 return <Badge count={itemCount} />;
30}
31 
typescript
1// src/navigation/AppNavigator.tsx
2import { createNativeStackNavigator } from '@react-navigation/native-stack';
3import { enableFreeze } from 'react-native-screens';
4 
5enableFreeze(true); // Freeze inactive screens
6 
7const Stack = createNativeStackNavigator<RootStackParamList>();
8 
9const ProductDetailScreen = React.lazy(() => import('../screens/ProductDetail'));
10const CheckoutScreen = React.lazy(() => import('../screens/Checkout'));
11 
12export function AppNavigator() {
13 return (
14 <Stack.Navigator screenOptions={{ animation: 'slide_from_right' }}>
15 <Stack.Screen name="Products" component={ProductListScreen} />
16 <Stack.Screen name="ProductDetail">
17 {(props) => (
18 <Suspense fallback={<LoadingScreen />}>
19 <ProductDetailScreen {...props} />
20 </Suspense>
21 )}
22 </Stack.Screen>
23 <Stack.Screen name="Checkout">
24 {(props) => (
25 <Suspense fallback={<LoadingScreen />}>
26 <CheckoutScreen {...props} />
27 </Suspense>
28 )}
29 </Stack.Screen>
30 </Stack.Navigator>
31 );
32}
33 

Testing Performance

typescript
1// src/__tests__/performance.test.tsx
2import { render } from '@testing-library/react-native';
3import { ProductListScreen } from '../screens/ProductList';
4 
5describe('ProductList Performance', () => {
6 it('renders 100 items within 100ms', () => {
7 const products = generateMockProducts(100);
8
9 const start = Date.now();
10 const { getByTestId } = render(
11 <TestWrapper>
12 <ProductListScreen testProducts={products} />
13 </TestWrapper>,
14 );
15 const elapsed = Date.now() - start;
16
17 expect(elapsed).toBeLessThan(100);
18 });
19});
20 

Conclusion

Building a performant React Native application follows a clear pattern: pre-compute data outside the render cycle, memoize list items and expensive components, use FlashList for virtualized lists, Reanimated for animations, and granular state selectors to prevent cascading re-renders. Each technique is independent and can be adopted incrementally.

The most common mistake is optimizing the wrong thing. Always measure first with React DevTools Profiler and Flipper, identify the actual bottleneck, then apply the appropriate technique. A single React.memo on a list item component often provides more improvement than a week of micro-optimizations elsewhere.

FAQ

Need expert help?

Building with mobile/frontend?

I help teams ship production-grade systems. From architecture review to hands-on builds.

Muneer Puthiya Purayil

SaaS Architect & AI Systems Engineer. 10+ years shipping production infrastructure across fintech, automotive, e-commerce, and healthcare.

Engage

Start a
Conversation.

For teams building at scale: SaaS platforms, agentic AI systems, and enterprise mobile infrastructure. Scope and fit are evaluated before any engagement begins.

Limited availability · Q3 / Q4 2026