1. FlatList Virtualization
FlatList already virtualizes by default — it only renders items near
the visible viewport — but a few props make a real, measurable difference on long
or complex lists.
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={renderItem}
getItemLayout={(data, index) => (
{ length: ROW_HEIGHT, offset: ROW_HEIGHT * index, index }
)}
windowSize={5}
maxToRenderPerBatch={10}
removeClippedSubviews
/>
getItemLayout tells FlatList each row's exact position without having
to measure it after render — for a fixed-height row, this is the single highest-leverage
tuning prop, and it's also what makes scrollToIndex reliable.
windowSize and maxToRenderPerBatch trade memory for
smoother scrolling on long lists; lower them on memory-constrained lists, raise them
if scrolling shows blank cells before content pops in.
2. Profiling & Preventing Re-renders
React DevTools' Profiler (usable against a React Native app the same way as web) highlights components that re-render more than their visible output justifies — usually because a parent re-renders and passes a new inline function or object as a prop every time.
<FlatList
data={items}
renderItem={({ item }) => <Row item={item} onPress={() => select(item.id)} />}
/>
const Row = memo(function Row({ item, onPress }: RowProps) {
return <Pressable onPress={() => onPress(item.id)}><Text>{item.title}</Text></Pressable>;
});
const renderItem = useCallback(
({ item }) => <Row item={item} onPress={handleSelect} />,
[handleSelect]
);
memo only helps if the props it receives are themselves stable —
wrapping Row in memo while still passing a fresh inline
arrow function as onPress every render defeats the optimization
entirely, since the props object is "different" on every render regardless.
3. Hermes
Hermes is the JS engine React Native ships with by default — instead of parsing and compiling JavaScript at app startup, it precompiles to bytecode at build time, so the app on the device just loads and runs bytecode directly.
The practical effect is faster startup (time-to-interactive), a smaller memory footprint, and a smaller app size, compared to shipping raw JS and compiling it on first launch. It's enabled by default on modern Expo/React Native projects — there's nothing to configure to benefit from it, but it's worth knowing it's the reason a cold start feels as fast as it does.
4. Reducing Bundle Size
npx expo export
npx source-map-explorer dist/_expo/static/js/ios/*.js.map
source-map-explorer renders the bundle as a visual, sized treemap —
the fastest way to find a single oversized dependency, a duplicated package pulled
in by two different libraries at different versions, or an entire library imported
for one small function.
- Import only what's used —
import { debounce } from 'lodash-es', not the whole library. - Check for duplicate versions of the same dependency in the tree (common with deeply nested transitive dependencies).
- Lazy-load screens that aren't needed on first launch with
React.lazywhere your navigation setup supports it.
5. Hands-on Exercise
Profile and optimize a slow screen
Take the longest list screen built so far and measurably improve it.
Requirements:
- Pick a list screen from an earlier week (posts, notes, or cart) and seed it with at least 200 items for testing.
- Record a React DevTools Profiler session scrolling and interacting with the unoptimized list; note which components re-render on every scroll or interaction.
- Apply
getItemLayout(for a fixed-height row),memoon the row component, and a stablerenderItem/onPressviauseCallback. - Re-record the same interaction and compare re-render counts and dropped frames before and after.
- Run
npx expo exportandsource-map-explorer, and identify the single largest contributor to the JS bundle.
If memo doesn't reduce re-renders even after wrapping the row component, check every prop it receives for hidden instability — an inline object literal (style={{ margin: 8 }}) or array creates a new reference every render just like an inline function does, and defeats memo's shallow comparison the same way.
6. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why does getItemLayout make scrollToIndex reliable, where FlatList without it can misbehave?
Why does getItemLayout make scrollToIndex reliable, where FlatList without it can misbehave?
Without getItemLayout, FlatList has to estimate an item's position (since it hasn't measured items outside the render window) and can jump or misjudge on scrollToIndex. Providing exact, computable layout tells it precisely where every item sits without measuring, which both speeds up rendering and makes programmatic scrolling land exactly right.
Q2
Why does wrapping a row component in memo sometimes fail to reduce re-renders at all?
Why does wrapping a row component in memo sometimes fail to reduce re-renders at all?
memo skips a re-render only when all props are shallowly equal to the previous render — if a parent still passes a fresh inline function or object as a prop every render (a new reference every time, even with identical contents), memo's comparison always finds a difference and re-renders anyway. The fix requires stabilizing every prop, typically with useCallback/useMemo, not just wrapping the child.
Q3
What does Hermes precompiling JS to bytecode at build time save, compared to compiling on the device at launch?
What does Hermes precompiling JS to bytecode at build time save, compared to compiling on the device at launch?
It removes the parse-and-compile step from the critical path of app startup — the device loads and executes ready-made bytecode directly instead of doing that work itself on every cold start, which measurably speeds up time-to-interactive and also reduces memory use and app size.
Q4
What is source-map-explorer actually showing you, and why is it more useful than just checking overall bundle size?
What is source-map-explorer actually showing you, and why is it more useful than just checking overall bundle size?
It visualizes which specific modules and dependencies make up the bundle and how large each one is, as a sized treemap — a single overall size number tells you the bundle is big, but not why. The treemap points directly at the actual offender (an oversized library, a duplicated dependency), which is what you need to know to fix it.