📅 Day 19
💡 tip

7 Essential React Native Performance Tips

Boost your React Native app performance with these 7 battle-tested tips - from startup optimization to fixing memory leaks and measuring real-world performance

PerformanceDebugging

Welcome to Day 19 of the React Native Advent Calendar!

Today we’re diving into 7 essential performance tips that will make your React Native apps faster, smoother, and more responsive. These are battle-tested strategies from real-world apps!

Todays tips are taken from my article React Native performance tactics: Modern strategies and tools and condensed into 7 short tips.


TIP 1: Optimize App Startup

The problem: Slow Time to Interactive (TTI) makes users think your app is broken.

The fix:

Use Code Splitting with Expo Router

Enable async routes to split your bundle:

app.json
{
	"experiments": {
		"asyncRoutes": true
	}
}

Analyze Your Bundle

Find what’s bloating your startup:

terminal
# Visualize bundle size with Expo Atlas
EXPO_ATLAS=true npx expo start

Profile Startup Performance

terminal
# Open DevTools and press 'j' to open the Profiler
npx expo start
# Press 'j' → Profiler tab → Record startup

Quick wins:

  • Defer non-critical imports
  • Lazy load heavy screens
  • Move expensive computations to after first render

TIP 2: Stop Unnecessary Renders

The problem: Components re-rendering when nothing changed = wasted CPU cycles.

Enable React Compiler (Expo SDK 54+)

Automatic memoization without useMemo or React.memo:

app.json
{
	"experiments": {
		"reactCompiler": true
	}
}

Visualize Re-renders

DevTools
# In React DevTools:
# 1. Open Settings (gear icon)
# 2. Enable "Highlight updates when components render"
# 3. Watch your app - flashing boxes = re-renders

Manual Optimization (Pre-Compiler)

components/UserCard.tsx
import { memo } from 'react';

// Only re-renders if props.user changes
export const UserCard = memo(({ user }) => {
	return (
		<View>
			<Text>{user.name}</Text>
		</View>
	);
});

TIP 3: Fix Animation Jank

The problem: Animations stutter because they run on the JS thread.

The fix: Use Reanimated to run animations on the UI thread.

Before (Janky)

SlowAnimation.tsx
// ❌ Runs on JS thread - stutters during heavy work
const [position] = useState(new Animated.Value(0));

Animated.timing(position, {
	toValue: 100,
	duration: 300,
	useNativeDriver: true
}).start();

After (Smooth)

SmoothAnimation.tsx
// ✅ Runs on UI thread - always 60fps
import { useSharedValue, withTiming } from 'react-native-reanimated';

const position = useSharedValue(0);

// Runs on UI thread even during heavy JS work
position.value = withTiming(100, { duration: 300 });

Monitor FPS in Real-Time

terminal
# Enable Performance Monitor
npx expo start
# Press 'd' → Show Performance Monitor
# Watch FPS while testing animations

TIP 4: Lists That Don’t Suck

The problem: Rendering 1000 items at once = instant crash.

The fix: Use FlashList (or optimized FlatList).

Use FlashList

terminal
npx expo install @shopify/flash-list
UserList.tsx
import { FlashList } from '@shopify/flash-list';

export function UserList({ users }) {
	return (
		<FlashList
			data={users}
			renderItem={({ item }) => <UserCard user={item} />}
			estimatedItemSize={80}
			// That's it! FlashList handles optimization
		/>
	);
}

Optimize FlatList (if you can’t use FlashList)

UserList.tsx
<FlatList
	data={users}
	renderItem={({ item }) => <UserCard user={item} />}
	keyExtractor={(item) => item.id} // ✅ Stable keys
	removeClippedSubviews={true} // ✅ Unmount off-screen items
	maxToRenderPerBatch={10} // ✅ Render in batches
	windowSize={5} // ✅ Keep 5 screens worth of items
	getItemLayout={(data, index) => ({
		// ✅ Skip layout calculation if items have fixed height
		length: 80,
		offset: 80 * index,
		index
	})}
/>

TIP 5: Avoid Global State Overkill

The problem: Every state change triggers ALL subscribers to re-render.

The fix: Use granular state and selectors.

Bad: Global State Triggers Everything

store.ts
// ❌ Changing theme re-renders EVERY component using this store
export const useStore = create((set) => ({
	user: { name: 'John' },
	theme: 'light',
	cart: [],
	updateTheme: (theme) => set({ theme })
}));

Good: Select Only What You Need

components/Header.tsx
// ✅ Only re-renders when theme changes
export function Header() {
	const theme = useStore((state) => state.theme);
	return <View style={{ backgroundColor: theme === 'dark' ? '#000' : '#fff' }} />;
}

Better: Split Stores

stores/themeStore.ts
// ✅ Separate stores for separate concerns
export const useThemeStore = create((set) => ({
	theme: 'light',
	toggleTheme: () => set((s) => ({ theme: s.theme === 'light' ? 'dark' : 'light' }))
}));

export const useUserStore = create((set) => ({
	user: null,
	setUser: (user) => set({ user })
}));

TIP 6: Fix Memory Leaks

The problem: Timers, listeners, and subscriptions that never clean up = app crashes.

The fix: Always clean up in useEffect.

Common Memory Leaks

components/Timer.tsx
// ❌ Memory leak - interval never clears
useEffect(() => {
	const interval = setInterval(() => {
		console.log('Tick');
	}, 1000);
	// Missing cleanup!
}, []);

// ✅ Proper cleanup
useEffect(() => {
	const interval = setInterval(() => {
		console.log('Tick');
	}, 1000);

	return () => clearInterval(interval);
}, []);

Abort Fetch Requests

hooks/useUsers.ts
useEffect(() => {
	const controller = new AbortController();

	fetch('https://api.example.com/users', {
		signal: controller.signal
	})
		.then((res) => res.json())
		.then(setUsers)
		.catch((err) => {
			if (err.name !== 'AbortError') console.error(err);
		});

	return () => controller.abort(); // ✅ Cancel on unmount
}, []);

Clean Up Event Listeners

hooks/useKeyboard.ts
useEffect(() => {
	const subscription = Keyboard.addListener('keyboardDidShow', handleShow);

	return () => subscription.remove(); // ✅ Remove listener
}, []);

TIP 7: Measure Performance

The problem: You can’t improve what you don’t measure.

The fix: Use tools to track performance in development AND production.

Development Tools

React DevTools Profiler:

terminal
# Press 'j' in Expo Dev Tools → Profiler
# Record interaction → See flamegraph of renders

Performance Monitor:

terminal
# Press 'd' → Show Performance Monitor
# Watch: JS FPS, UI FPS, RAM usage

Quick Performance Checklist

Before shipping your app, verify:

  • ✅ TTI below 3 seconds
  • ✅ Animations run at 60fps
  • ✅ Lists scroll smoothly with 1000+ items
  • ✅ No memory leaks (test by navigating between screens 20+ times)
  • ✅ Bundle size optimized (check with Expo Atlas)
  • ✅ Production monitoring enabled (Sentry or similar)
  • ✅ Performance tested on low-end devices

Wrapping Up

Performance optimization isn’t a one-time task - it’s an ongoing process. These 7 tips cover the most common bottlenecks in React Native apps.

Quick recap:

  1. Optimize startup - Code split, defer imports, analyze bundle
  2. Stop unnecessary renders - Use React Compiler or manual memoization
  3. Fix animation jank - Use Reanimated for 60fps animations
  4. Lists that don’t suck - FlashList or optimized FlatList
  5. Avoid state overkill - Use selectors and split stores
  6. Fix memory leaks - Always clean up in useEffect
  7. Measure performance - DevTools + production monitoring

Ready to dive deeper?

Improved your app’s performance? Share your wins on Twitter!

Tomorrow we’ll explore Day 20’s topic - see you then! 🎄