While FlashList does its best to achieve high performance, it will still perform poorly if your item components are slow to render. This guide covers best practices for writing performant list item components.
Always profile performance in release mode. FlashList’s performance between JS dev mode and release mode differs greatly due to a much smaller render buffer in dev mode.
Before optimizing, understand what makes FlashList fast:
- View Recycling: Components are reused instead of created/destroyed
- Minimal Re-renders: Only visible items are rendered initially
- Efficient Updates: Smart diffing prevents unnecessary renders
Your item components should be optimized to work with these mechanisms, not against them.
Essential Optimizations
1. Remove key Props
Using key prop inside your item and item’s nested components will highly degrade performance.
Make sure your item components and their nested components don’t have a key prop. Using this prop will lead to FlashList not being able to recycle views, losing all the benefits of using it over FlatList.
Why Keys Harm FlashList
FlashList’s core performance advantage comes from recycling components. When you add a key prop that changes between different data items, React treats the component as entirely different and forces a complete re-creation of the component tree.
Example Problem:
// ❌ Bad: Keys prevent recycling
const MyNestedComponent = ({ item }) => {
return <Text key={item.id}>I am nested!</Text>; // Don't do this!
};
const MyItem = ({ item }) => {
return (
<View key={item.id}> {/* Don't do this! */}
<MyNestedComponent item={item} />
<Text>{item.title}</Text>
</View>
);
};
Correct Approach:
// ✅ Good: No keys, recycling works perfectly
const MyNestedComponent = ({ item }) => {
return <Text>I am nested!</Text>;
};
const MyItem = ({ item }) => {
return (
<View>
<MyNestedComponent item={item} />
<Text>{item.title}</Text>
</View>
);
};
Handling Required Keys (with .map)
When React forces you to use key prop (like when using .map), use the useMappingHelper hook:
import { useMappingHelper } from "@shopify/flash-list";
const MyItem = ({ item }) => {
const { getMappingKey } = useMappingHelper();
return (
<View>
{item.users.map((user, index) => (
<Text key={getMappingKey(index, user.id)}>
{user.name}
</Text>
))}
</View>
);
};
The useMappingHelper hook intelligently provides the right key strategy:
- Inside FlashList: Uses stable keys that don’t change during recycling
- Outside FlashList: Uses the provided item key for proper React reconciliation
This ensures components can be recycled properly while maintaining React’s reconciliation correctness.
2. Use getItemType for Heterogeneous Lists
If you have different types of cell components that are vastly different, leverage the getItemType prop.
When the list recycles items and the component type changes drastically (e.g., from a text message to an image), React won’t be able to optimize the re-render since the whole render tree changes.
Example Without getItemType:
enum MessageType {
Text,
Image,
}
interface TextMessage {
text: string;
type: MessageType.Text;
}
interface ImageMessage {
image: ImageSourcePropType;
type: MessageType.Image;
}
type Message = ImageMessage | TextMessage;
// ❌ Performance issue: Text components get recycled as Image and vice versa
const MessageItem = ({ item }: { item: Message }) => {
switch (item.type) {
case MessageType.Text:
return <Text>{item.text}</Text>;
case MessageType.Image:
return <Image source={item.image} />;
}
};
const MessageList = () => {
return (
<FlashList
renderItem={({ item }) => <MessageItem item={item} />}
data={messages}
/>
);
};
Correct Approach with getItemType:
// ✅ Good: Separate recycling pools for different types
const MessageList = () => {
return (
<FlashList
renderItem={({ item }) => <MessageItem item={item} />}
data={messages}
getItemType={(item) => item.type} // Separate recycling pools!
/>
);
};
FlashList will now use separate recycling pools based on item.type. Text messages will only be recycled as text messages, and images as images, making re-renders much faster.
getItemType is called very frequently. Keep it fast - just return a simple type identifier.
3. Memoize Expensive Computations
If you do any calculations that might take a lot of resources, consider memoizing them:
import { useMemo } from "react";
const MyItem = ({ item }) => {
// ✅ Expensive calculation is memoized
const processedData = useMemo(() => {
return complexDataProcessing(item.rawData);
}, [item.rawData]);
const formattedDate = useMemo(() => {
return new Date(item.timestamp).toLocaleDateString();
}, [item.timestamp]);
return (
<View>
<Text>{processedData}</Text>
<Text>{formattedDate}</Text>
</View>
);
};
Even better: Pre-compute expensive operations at the data level:
// ✅ Best: Compute once when data loads
const processedData = rawData.map((item) => ({
...item,
processedValue: complexDataProcessing(item.rawData),
formattedDate: new Date(item.timestamp).toLocaleDateString(),
}));
<FlashList
data={processedData}
renderItem={({ item }) => (
<View>
<Text>{item.processedValue}</Text>
<Text>{item.formattedDate}</Text>
</View>
)}
/>;
4. Memoize Leaf Components
Components that don’t directly depend on the item prop can be memoized to skip re-renders during recycling:
import { memo } from "react";
// Heavy component that doesn't depend on item
const MyHeavyComponent = () => {
return (
<View>
<ComplexVisualization />
<AnotherHeavyComponent />
</View>
);
};
// ✅ Memoize it to prevent re-renders
const MemoizedMyHeavyComponent = memo(MyHeavyComponent);
const MyItem = ({ item }) => {
return (
<View>
<MemoizedMyHeavyComponent /> {/* Won't re-render on recycle */}
<Text>{item.title}</Text>
</View>
);
};
For components that take props:
interface IconProps {
name: string;
size: number;
}
// Only re-renders if name or size changes
const MemoizedIcon = memo<IconProps>(({ name, size }) => {
return <Icon name={name} size={size} />;
});
const MyItem = ({ item }) => {
return (
<View>
<MemoizedIcon name="star" size={24} /> {/* Same props = no re-render */}
<Text>{item.title}</Text>
</View>
);
};
5. Use useCallback for Event Handlers
Memoize callbacks to prevent unnecessary re-renders of child components:
import { useCallback, memo } from "react";
interface ButtonProps {
onPress: () => void;
title: string;
}
const MemoizedButton = memo<ButtonProps>(({ onPress, title }) => {
return <Pressable onPress={onPress}><Text>{title}</Text></Pressable>;
});
const MyItem = ({ item, onItemPress }) => {
// ✅ Callback is memoized
const handlePress = useCallback(() => {
onItemPress(item.id);
}, [item.id, onItemPress]);
return (
<View>
<Text>{item.title}</Text>
<MemoizedButton onPress={handlePress} title="View Details" />
</View>
);
};
Advanced Optimizations
Memoize FlashList Props
Memoizing props passed to FlashList is more important in v2. We allow developers to ensure that props are memoized and will prevent re-renders of children wherever obvious.
import { useMemo, useCallback } from "react";
const MyList = () => {
const [data, setData] = useState(initialData);
const [selectedId, setSelectedId] = useState(null);
// ✅ Memoize renderItem
const renderItem = useCallback(
({ item }) => (
<MyItem
item={item}
isSelected={item.id === selectedId}
onPress={setSelectedId}
/>
),
[selectedId]
);
// ✅ Memoize keyExtractor
const keyExtractor = useCallback((item) => item.id, []);
// ✅ Memoize getItemType
const getItemType = useCallback((item) => item.type, []);
return (
<FlashList
data={data}
renderItem={renderItem}
keyExtractor={keyExtractor}
getItemType={getItemType}
/>
);
};
Optimize Image Loading
Images can significantly impact list performance:
import FastImage from "react-native-fast-image";
const MyItem = ({ item }) => {
return (
<View>
{/* ✅ Use FastImage for better performance */}
<FastImage
source={{ uri: item.imageUrl, priority: FastImage.priority.normal }}
style={{ width: 50, height: 50 }}
resizeMode={FastImage.resizeMode.cover}
/>
<Text>{item.title}</Text>
</View>
);
};
Avoid Anonymous Functions in Render
// ❌ Bad: Creates new function on every render
const MyItem = ({ item, onPress }) => {
return (
<Pressable onPress={() => onPress(item.id)}>
<Text>{item.title}</Text>
</Pressable>
);
};
// ✅ Good: Memoized callback
const MyItem = ({ item, onPress }) => {
const handlePress = useCallback(() => {
onPress(item.id);
}, [item.id, onPress]);
return (
<Pressable onPress={handlePress}>
<Text>{item.title}</Text>
</Pressable>
);
};
Simplify Component Structure
Flatter component hierarchies render faster:
// ❌ Bad: Deep nesting
const MyItem = ({ item }) => {
return (
<View>
<View>
<View>
<View>
<Text>{item.title}</Text>
</View>
</View>
</View>
</View>
);
};
// ✅ Good: Flatter structure
const MyItem = ({ item }) => {
return (
<View style={styles.container}>
<Text>{item.title}</Text>
</View>
);
};
Memoize Styles
import { useMemo } from "react";
const MyItem = ({ item }) => {
// ✅ Dynamic styles are memoized
const containerStyle = useMemo(
() => ({
backgroundColor: item.isHighlighted ? "#ffeb3b" : "transparent",
padding: 16,
}),
[item.isHighlighted]
);
return (
<View style={containerStyle}>
<Text>{item.title}</Text>
</View>
);
};
// ✅ Even better: Use static styles when possible
const styles = StyleSheet.create({
container: {
padding: 16,
},
highlighted: {
backgroundColor: "#ffeb3b",
},
});
const MyItem = ({ item }) => {
return (
<View style={[styles.container, item.isHighlighted && styles.highlighted]}>
<Text>{item.title}</Text>
</View>
);
};
Complete Example
Here’s a complete example incorporating all best practices:
import React, { memo, useCallback, useMemo } from "react";
import { View, Text, Pressable, StyleSheet, Image } from "react-native";
import { FlashList } from "@shopify/flash-list";
import { useRecyclingState } from "@shopify/flash-list";
// ✅ Memoized child component
const Avatar = memo<{ uri: string; size: number }>(({ uri, size }) => {
return (
<Image
source={{ uri }}
style={{ width: size, height: size, borderRadius: size / 2 }}
/>
);
});
// ✅ Separate types use getItemType
enum ItemType {
Header = "header",
User = "user",
Footer = "footer",
}
interface HeaderItem {
type: ItemType.Header;
title: string;
}
interface UserItem {
type: ItemType.User;
id: string;
name: string;
avatarUrl: string;
bio: string;
}
interface FooterItem {
type: ItemType.Footer;
text: string;
}
type ListItem = HeaderItem | UserItem | FooterItem;
// ✅ Static styles
const styles = StyleSheet.create({
headerContainer: {
padding: 16,
backgroundColor: "#f5f5f5",
},
userContainer: {
flexDirection: "row",
padding: 16,
alignItems: "center",
},
userInfo: {
marginLeft: 12,
flex: 1,
},
expanded: {
backgroundColor: "#e3f2fd",
},
});
// ✅ Optimized item component
const ListItemComponent = memo<{
item: ListItem;
onUserPress: (id: string) => void;
}>(({ item, onUserPress }) => {
// ✅ State resets on recycle
const [isExpanded, setIsExpanded] = useRecyclingState(
false,
[item.type === ItemType.User ? item.id : null]
);
// ✅ Memoized callback
const handlePress = useCallback(() => {
if (item.type === ItemType.User) {
setIsExpanded(!isExpanded);
onUserPress(item.id);
}
}, [item, isExpanded, onUserPress]);
// Render different types
if (item.type === ItemType.Header) {
return (
<View style={styles.headerContainer}>
<Text style={{ fontSize: 18, fontWeight: "bold" }}>{item.title}</Text>
</View>
);
}
if (item.type === ItemType.Footer) {
return (
<View style={styles.headerContainer}>
<Text>{item.text}</Text>
</View>
);
}
// User item
return (
<Pressable
onPress={handlePress}
style={[styles.userContainer, isExpanded && styles.expanded]}
>
<Avatar uri={item.avatarUrl} size={48} />
<View style={styles.userInfo}>
<Text style={{ fontWeight: "600" }}>{item.name}</Text>
{isExpanded && <Text style={{ marginTop: 4 }}>{item.bio}</Text>}
</View>
</Pressable>
);
});
// ✅ Optimized list component
export const UserList = ({ data }: { data: ListItem[] }) => {
// ✅ Memoized callbacks
const handleUserPress = useCallback((id: string) => {
console.log("User pressed:", id);
}, []);
const renderItem = useCallback(
({ item }: { item: ListItem }) => (
<ListItemComponent item={item} onUserPress={handleUserPress} />
),
[handleUserPress]
);
const keyExtractor = useCallback((item: ListItem) => {
if (item.type === ItemType.User) return item.id;
return item.type;
}, []);
const getItemType = useCallback((item: ListItem) => item.type, []);
return (
<FlashList
data={data}
renderItem={renderItem}
keyExtractor={keyExtractor}
getItemType={getItemType}
/>
);
};
Before considering your component optimized, verify:
Use React DevTools Profiler to identify slow components:
import { Profiler } from "react";
const onRenderCallback = (
id: string,
phase: "mount" | "update",
actualDuration: number
) => {
console.log(`${id} (${phase}) took ${actualDuration}ms`);
};
<Profiler id="FlashList" onRender={onRenderCallback}>
</Profiler>;
Always measure performance before and after optimizations. Not all optimizations provide meaningful benefits, and some can even make things worse.
Summary
- Remove
key props from item components (use useMappingHelper for .map)
- Use
getItemType for heterogeneous lists
- Memoize everything: computations, callbacks, components, and FlashList props
- Simplify structure and avoid deep nesting
- Test in release mode - dev mode performance is not representative
- Profile and measure - only optimize what’s actually slow
By following these practices, your FlashList will render smoothly even with complex item components.