React Native Performance Tips - Real-world Examples
Practical optimization examples for React Native apps that you can use today
Real performance patterns from production React Native apps.
FlatList Optimization
Lists show up on most screens. The patterns below are the ones that actually move the needle.
// Bad: new functions on every render
const BadContactList = ({ contacts }) => {
return (
<FlatList
data={contacts}
renderItem={({ item }) => (
<ContactCard
contact={item}
onPress={() => handlePress(item.id)} // new function every render
/>
)}
keyExtractor={item => item.id} // also new every render
/>
);
};
// Good: memoized
const ContactList = ({ contacts }) => {
// Memoize the renderItem function
const renderItem = useCallback(({ item }) => (
<ContactCard
contact={item}
onPress={handlePressContact} // Use shared function
/>
), []); // Empty deps if function doesn't use any props/state
// Memoize the keyExtractor
const keyExtractor = useCallback((item) => item.id, []);
// Memoize onEndReached handler if you're doing pagination
const handleEndReached = useCallback(() => {
if (!isLoading && hasMorePages) {
loadMoreContacts();
}
}, [isLoading, hasMorePages]);
return (
<FlatList
data={contacts}
renderItem={renderItem}
keyExtractor={keyExtractor}
onEndReached={handleEndReached}
onEndReachedThreshold={0.5}
// Performance props
removeClippedSubviews={true}
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={5}
ListEmptyComponent={EmptyListMessage}
contentContainerStyle={styles.listContent}
/>
);
};
// Memoize the entire CardItem component
const ContactCard = memo(({ contact, onPress }) => (
<TouchableOpacity
onPress={() => onPress(contact.id)}
style={styles.card}
>
<FastImage
source={{ uri: contact.avatar }}
style={styles.avatar}
/>
<View style={styles.info}>
<Text style={styles.name}>{contact.name}</Text>
<Text style={styles.phone}>{contact.phone}</Text>
</View>
</TouchableOpacity>
));
Image-heavy screens
Pattern I use for profile and gallery screens:
const ProfileScreen = () => {
// Memoize expensive filter operations
const sortedPhotos = useMemo(() =>
photos
.filter(p => p.userId === currentUserId)
.sort((a, b) => b.date - a.date)
, [photos, currentUserId]
);
// Memoize image picking function
const handleImagePick = useCallback(async () => {
try {
const result = await ImagePicker.launchImageLibrary({
mediaType: 'photo',
quality: 0.8,
});
if (result.assets?.[0]?.uri) {
const compressed = await compressImage(result.assets[0].uri);
await uploadImage(compressed);
}
} catch (error) {
// Error handling
}
}, []);
return (
<View style={styles.container}>
<ProfileHeader
user={user}
onImagePress={handleImagePick}
/>
<PhotoGrid photos={sortedPhotos} />
</View>
);
};
// Memoized grid component
const PhotoGrid = memo(({ photos }) => {
const renderPhoto = useCallback(({ item }) => (
<FastImage
source={{ uri: item.uri }}
style={styles.gridPhoto}
resizeMode="cover"
/>
), []);
return (
<FlatList
data={photos}
renderItem={renderPhoto}
numColumns={3}
removeClippedSubviews={true}
/>
);
});
Forms
Use refs instead of state when you don’t need per-keystroke validation:
const EditProfileForm = () => {
// Use refs instead of state for form fields when you don't need
// real-time validation
const nameRef = useRef();
const bioRef = useRef();
const emailRef = useRef();
// Memoize submission handler
const handleSubmit = useCallback(async () => {
const formData = {
name: nameRef.current?.value,
bio: bioRef.current?.value,
email: emailRef.current?.value,
};
try {
await updateProfile(formData);
navigation.goBack();
} catch (error) {
// Error handling
}
}, [navigation]);
// If you need validation, memoize the validation function
const validateField = useCallback((field, value) => {
switch (field) {
case 'email':
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
case 'name':
return value.length >= 2;
default:
return true;
}
}, []);
return (
<KeyboardAvoidingView style={styles.container}>
<TextInput
ref={nameRef}
placeholder="Name"
style={styles.input}
/>
<TextInput
ref={bioRef}
placeholder="Bio"
multiline
style={styles.bioInput}
/>
<TextInput
ref={emailRef}
placeholder="Email"
keyboardType="email-address"
style={styles.input}
/>
<Button title="Save" onPress={handleSubmit} />
</KeyboardAvoidingView>
);
};
Navigation
Memoize handlers and any data prep for the next screen:
const HomeScreen = () => {
// Memoize navigation handlers
const handleProfilePress = useCallback(() => {
navigation.navigate('Profile', {
userId: currentUserId,
});
}, [currentUserId, navigation]);
// Memoize data preparation for the next screen
const prepareDataForNextScreen = useCallback((item) => ({
title: item.title,
id: item.id,
preview: item.images[0],
// Transform any other data needed
}), []);
const handleItemPress = useCallback((item) => {
const screenData = prepareDataForNextScreen(item);
navigation.navigate('Details', screenData);
}, [navigation, prepareDataForNextScreen]);
return (
<View style={styles.container}>
<Header onProfilePress={handleProfilePress} />
<FeedList
data={feedItems}
onItemPress={handleItemPress}
/>
</View>
);
};
Misc tips
- Use
memofor list items in FlatList/ScrollView:
const ListItem = memo(({ title, onPress }) => (
<TouchableOpacity onPress={onPress}>
<Text>{title}</Text>
</TouchableOpacity>
), (prevProps, nextProps) => {
// Custom comparison if needed
return prevProps.title === nextProps.title;
});
- Handle animations efficiently:
const FadeInView = ({ children }) => {
const opacity = useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.timing(opacity, {
toValue: 1,
duration: 500,
useNativeDriver: true,
}).start();
}, []);
return (
<Animated.View style={{ opacity }}>
{children}
</Animated.View>
);
};
When not to bother
Skip the optimizations when:
- The list has fewer than 20 items
- The form has 2-3 fields
- It’s a prototype
- The screen is rarely visited
0 claps
If this was useful, let me know.