Performance optimisation
Stop unnecessary re-renders, use memo and useCallback deliberately, understand Hermes, tune long lists, and profile real frames instead of guessing.
Re-render control
import { memo, useCallback, useMemo, useState } from 'react';
import { FlatList, Pressable, Text } from 'react-native';
const Row = memo(function Row({
item,
onOpen,
}: {
item: { id: string; title: string };
onOpen: (id: string) => void;
}) {
return (
<Pressable onPress={() => onOpen(item.id)}>
<Text>{item.title}</Text>
</Pressable>
);
});
export function List({ items }: { items: { id: string; title: string }[] }) {
const [query, setQuery] = useState('');
// stable identity so memo actually prevents re-renders
const handleOpen = useCallback((id: string) => console.log('open', id), []);
const filtered = useMemo(
() => items.filter((i) => i.title.toLowerCase().includes(query.toLowerCase())),
[items, query],
);
return (
<FlatList
data={filtered}
keyExtractor={(i) => i.id}
renderItem={({ item }) => <Row item={item} onOpen={handleOpen} />}
/>
);
}memocompares props shallowly; it only helps if every prop is referentially stable across renders.- An inline arrow function or object literal prop defeats
memocompletely — the classic reason memo appears to do nothing. - Do not memo everything: the comparison has a cost and a premature memo makes the code harder to read.
- Use React DevTools' highlight-updates option to see which components actually re-render before you optimise.
Hermes and startup cost
| Change | Effect | Cost |
|---|---|---|
| Enable Hermes | Faster startup, lower memory | Some debugging tooling differences |
| Inline requires | Defers module evaluation to first use | Slightly later errors during development |
| Hermes bytecode bundle | No parse at launch | An extra build step |
| Reduce provider depth | Fewer re-renders at the root | Some refactoring |
| Avoid large synchronous work at startup | First frame sooner | Move to a task after mount |
// metro.config.js: defer expensive modules until they are used
module.exports = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
};Measure startup with a custom trace rather than by feel. Time from process start to the first interactive frame, and track it per release; a regression of 300ms is invisible in development and obvious to users.
Long lists and images
import { Image } from 'expo-image';
export const Thumb = Image;
<Thumb
source={{ uri: item.imageUrl }}
style={{ width: 72, height: 72, borderRadius: 8 }}
contentFit="cover"
transition={150}
cachePolicy="memory-disk"
recyclingKey={item.id}
/>;
// fixed-height rows: skip measurement entirely
<FlatList
getItemLayout={(_, index) => ({ length: 88, offset: 88 * index, index })}
initialNumToRender={10}
maxToRenderPerBatch={8}
windowSize={5}
updateCellsBatchingPeriod={50}
/>;💡
Decode images at display size. A 4000 pixel photo shown in a 72 pixel thumbnail costs more than fifty times the memory it needs, and on a long list that is the difference between a smooth scroll and a crash.
FAQ
Why does memo not stop my row from re-rendering?
One of its props is a new reference every render — usually an inline arrow function or a freshly built object. Memoise the callback with
useCallback and pass only the data the row needs.Is the New Architecture faster?
It removes the asynchronous bridge for most calls and makes synchronous native access possible, which helps gesture and list performance. The bigger win for most apps is still fewer re-renders and correctly sized images.
Related
Lists, forms and input handling Testing React Native apps
Last refreshed 2026-09-18.