Debugging & Performance Optimization
A common interview topic — find slow spots with DevTools, stop wasted re-renders, and tune long FlatLists for buttery scrolling.
What you will learn
- Debug a React Native app with the DevTools
- Cut wasted re-renders with memo, useCallback and useMemo
- Tune a FlatList and know what the New Architecture is
Seeing what your app is doing
When something is slow or buggy, you need to see what is happening, not guess. React Native ships with a debugger: shake the phone (or press a shortcut in the simulator) to open the dev menu, then open the React DevTools and the JS debugger. There you can read console.log output, inspect the component tree, and use the Profiler to see which components re-render and how long they take. (Older projects used a separate tool called Flipper; the built-in DevTools have largely replaced it.)
The single most common performance problem is wasted re-renders: a component redrawing when nothing it shows actually changed. A re-render is React running a component again to update the screen — usually good, but if it happens needlessly on a heavy component or a long list, the app stutters.
Cutting wasted re-renders
Three tools, used together, stop most wasted work. Here is what each one is for:
| Tool | What it does |
|---|---|
React.memo | Skips re-rendering a component if its props did not change |
useCallback | Remembers a function so it is not recreated every render (keeps memo working) |
useMemo | Remembers the result of an expensive calculation between renders |
The idea, in plain terms: memo wraps a child so it only re-renders when its props really change. But a function prop is recreated on every parent render, which would defeat memo — so you wrap that function in useCallback to keep it stable. Here is the pattern:
import { memo, useCallback, useState } from 'react';
import { Text, Button, View } from 'react-native';
// Child only re-renders if its props change, thanks to memo
const Row = memo(function Row({ label, onPress }) {
console.log('rendering', label);
return <Button title={label} onPress={onPress} />;
});
export default function App() {
const [count, setCount] = useState(0);
// Stable function — not recreated each render, so memo keeps working
const handlePress = useCallback(() => console.log('pressed'), []);
return (
<View>
<Text>{count}</Text>
<Button title="Re-render parent" onPress={() => setCount(count + 1)} />
<Row label="I should not re-render" onPress={handlePress} />
</View>
);
}Note: Output (in the console):
The first render logs: rendering I should not re-render
Then tap "Re-render parent" several times -> the count changes but that line does not print again.
Without memo and useCallback, the Row would log on every tap even though nothing about it changed — exactly the wasted work that slows real apps.
Tuning a FlatList
Long lists are the other big performance area. FlatList is already efficient, but a few props help with thousands of rows or heavy item content:
- Give every item a stable
keyExtractorso rows are reused, not rebuilt. - If all rows are the same height, add
getItemLayoutso the list can jump without measuring each row. - Wrap the row component in
React.memoso unchanged rows do not re-render while scrolling. - Tune
initialNumToRender(how many rows to draw first) andwindowSize(how much off-screen to keep ready) for a smoother first paint.
A word on the New Architecture
You may see "New Architecture", "Fabric" and "TurboModules" in job posts. In plain terms: React Native rebuilt how JavaScript talks to the native side to make it faster and smoother. Fabric is the new rendering system; TurboModules is the faster way native modules load. As an app developer you mostly do not change your code for it — modern Expo enables it for you. You just need to know what it is and that newer Expo versions use it by default.
A debugging workflow, step by step
When an app feels slow, work through it in this order rather than guessing:
- Open the dev menu (shake the device or the simulator shortcut) and launch the React DevTools Profiler.
- Record while you do the slow action, then look at which components re-rendered and how long they took.
- If a heavy component re-renders when its data did not change, wrap it in
React.memoand stabilise any function props withuseCallback. - If a calculation is expensive and runs every render, wrap it in
useMemoso it only recomputes when its inputs change. - If a long list janks while scrolling, apply the FlatList tips above (
keyExtractor,memorows,getItemLayout). - Re-profile to confirm the wasted renders are gone — measure, do not assume.
Tip: Optimise only what the Profiler shows is slow. Adding memo/useCallback everywhere "just in case" makes code harder to read for no benefit — reach for them when a measurement points at a real hot spot.
Watch out: Performance bugs feel different in development. The dev build is slower than the real app, so always confirm a "slow" feeling in a production build (or at least with the JS debugger closed) before optimising — you may be chasing a problem users never see.
Q. What is the main job of React.memo on a component?
React.memo memoises a component so it only re-renders when its props actually change — cutting the wasted re-renders that commonly slow React Native apps.✍️ Practice
- Wrap a child component in
React.memoand confirm with aconsole.logthat it stops re-rendering when the parent updates unrelated state. - Add
getItemLayoutand a memoised row to a FlatList and note any scrolling difference.
🏠 Homework
- Profile one of your earlier screens, find a component that re-renders needlessly, fix it with
memoanduseCallback, and write one line on what you changed.