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} />}
    />
  );
}
  • memo compares props shallowly; it only helps if every prop is referentially stable across renders.
  • An inline arrow function or object literal prop defeats memo completely — 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

ChangeEffectCost
Enable HermesFaster startup, lower memorySome debugging tooling differences
Inline requiresDefers module evaluation to first useSlightly later errors during development
Hermes bytecode bundleNo parse at launchAn extra build step
Reduce provider depthFewer re-renders at the rootSome refactoring
Avoid large synchronous work at startupFirst frame soonerMove 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.

Lists, forms and input handling Testing React Native apps

Last refreshed 2026-09-18.