Skip to main content
Back to module
Mobile App Production · Quality

Performance

Reading58mLesson 2 of 3 in this module

Measure frame rates and renders, then fix a slow list with keys, memoization and windowing.

Why it matters

Users notice slowness more on phones than anywhere else: a list that stutters while scrolling, a button that responds a beat late, an app that takes four seconds to open. Many users own older, cheaper Android devices, so an app that feels fine on a new iPhone can be painful for them. Performance work is a mid-level skill because it requires measuring first and fixing the right thing.

Frames and threads

Smooth motion means rendering a frame about every 16.7 ms at 60 frames per second (8.3 ms on 120 Hz screens). React Native work happens mainly on:

  • the JavaScript thread, where your components render and your logic runs,
  • the UI (main) thread, where native views are laid out and drawn.

If the JS thread is busy for 200 ms (a big re-render, a heavy calculation), taps and JS-driven animations freeze. If the UI thread is overloaded (huge view trees, big images), scrolling drops frames.

Measure first

  • The Performance Monitor in the developer menu shows JS and UI frame rates live.
  • React DevTools Profiler (press j in the Expo CLI to open the debugger tools) records which components rendered and how long they took, and why.
  • Test in a release-like build: development mode is much slower. Run npx expo start --no-dev --minify or use a preview build.
  • Test on a low-end Android phone or emulator profile, not only your own device.

Write down a number before you change anything ("JS frame rate drops to 20 fps while scrolling 1,000 rows") so you can tell whether a fix helped.

Worked example: fixing a slow list

A screen shows 2,000 transactions. Scrolling stutters and the profiler shows every row re-rendering whenever the parent's state changes.

// Before
<FlatList
  data={transactions}
  renderItem={({ item }) => <Row item={item} onPress={() => open(item.id)} />}
/>

Fixes, in the order they usually matter:

  1. Stable keys: add keyExtractor={(t) => t.id} so rows are not recreated.
  2. Memoized rows: wrap the row in React.memo and pass stable props. Creating a new onPress arrow for every row each render defeats memo, so pass the id and a stable callback:
const Row = React.memo(function Row({ item, onOpen }) {
  return (
    <Pressable onPress={() => onOpen(item.id)} style={styles.row}>
      <Text>{item.title}</Text>
      <Text>{formatMoney(item.amount)}</Text>
    </Pressable>
  );
});

const onOpen = useCallback((id) => router.push(`/tx/${id}`), []);
const renderItem = useCallback(({ item }) => <Row item={item} onOpen={onOpen} />, [onOpen]);

<FlatList
  data={transactions}
  keyExtractor={(t) => t.id}
  renderItem={renderItem}
  getItemLayout={(_, index) => ({ length: 64, offset: 64 * index, index })}
  initialNumToRender={12}
  windowSize={7}
/>
  1. Fixed row height: if every row is the same height, getItemLayout lets the list skip measuring.
  2. Tune windowing: initialNumToRender and windowSize control how many rows are kept rendered; smaller means less work and more blank space if users fling fast.
  3. Do less per row: pre-format values once (when data arrives) instead of in every render, and use correctly sized thumbnails instead of full-size images.
  4. Consider FlashList (@shopify/flash-list), which recycles row views and is often faster for very long lists.

Then measure again with the same steps.

Other common wins

  • Startup: load only what the first screen needs; defer heavy imports and work until after first render; keep the splash screen short.
  • Images: serve thumbnails at display size; expo-image adds caching.
  • Animations: run them on the UI thread (useNativeDriver: true with Animated, or Reanimated) so a busy JS thread does not stutter them.
  • Re-renders: split contexts so a frequently changing value does not re-render the whole tree.
  • Remove console.log in hot paths: logging is slow, especially in development.

Common mistakes

  • Optimizing without measuring, or measuring in development mode only.
  • ScrollView with map for long lists.
  • Inline arrow functions and objects as props to memoized rows.
  • useMemo and useCallback everywhere "just in case": they have a cost too. Use them where the profiler shows a problem.

Quick recap

  • Aim for 60 fps: keep both the JS and UI threads under their frame budget.
  • Measure with the Performance Monitor and React DevTools Profiler, in a release-like build, on a slow device.
  • For lists: keys, memoized rows with stable props, getItemLayout, windowing, lighter rows, or FlashList.

Try it

The Do tab walks you through building a deliberately slow list, profiling it and fixing it step by step.

Also in this lesson: