All writing
React Native

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>
  );
};

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

  1. Use memo for 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;
});
  1. 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:

0 claps
If this was useful, let me know.
Optimizing React Forms - Beyond useState Prevent the Keyboard from Covering React Native UI Components