←Back to blog
Article

FlatList with image-heavy rows on a cheap Android phone

FlatList with image-heavy rows on a cheap Android phone
S

StriveBit

4 min readMobile Development

FlatList with image-heavy rows on a low-end Android phone

Take a field reporting app that lets warehouse staff photograph incoming shipments. The list screen shows forty rows, each with a 120×120 thumbnail, a reference number, and a timestamp. On a low-end test phone — a Redmi 7A with 2GB RAM on Android 9, the sort of device we keep around for exactly this — scrolling through a list like that can drop to roughly 14 frames per second after the first ten rows. Cheap field tablets behave the same way.

The first instinct is to blame the network. The images load from a CDN, so it is natural to assume decode work is piling up behind slow downloads. But the FPS drop happens even with all forty images pre-cached to local storage. The bottleneck is not the network. It is the image decode and the way React Native's image component handles resizing.

Suppose the thumbnails are stored as 1200×900 JPEGs served by the CDN at that resolution. React Native's Image component decodes the full bitmap into memory, then scales it down to the 120×120 display size. On a 2GB device, that means forty 3.4MB bitmaps competing for memory. The garbage collector thrashes, the JS thread stalls, and the frame rate collapses.

There are four things worth trying, in order, measuring each with the React Native Performance Monitor overlay (shake menu → Show Perf Monitor). Scroll the list at a steady rate for ten seconds and read the average JS and UI thread FPS. The numbers below are the kind we have seen on a device of this class; yours will differ, but the ordering tends to hold.

From a baseline of around 14 FPS on the UI thread, adding `getItemLayout` to skip measurement improves nothing — layout is not the problem. The real fix comes from requesting correctly sized images from the CDN. Add a query parameter to the image URL so the CDN serves a 240×180 version instead of the full 1200×900. That alone can take a list like this into the high 30s.

The second improvement is `removeClippedSubviews`, which is specifically useful on Android. It keeps off-screen row views detached from the native hierarchy. Expect another handful of frames per second from that, into the low 40s.

We also set `maxToRenderPerBatch` to 2 and `windowSize` to 5. The defaults are 8 and 21. On a fast phone you never notice the defaults cause extra work. On a 2GB device, reducing them means fewer rows decoded at once. The tradeoff is visible blank space during fast scrolls. We accept that because the alternative is a list nobody can use.

The final change is `fadeDuration={0}` on the Image component. The default fade-in animation on Android triggers a re-render per image as it loads from cache. Disabling it removes a noticeable stutter.

<FlatList
  data={shipments}
  keyExtractor={(item) => item.id}
  renderItem={renderRow}
  getItemLayout={(_, index) => ({
    length: ROW_HEIGHT,
    offset: ROW_HEIGHT * index,
    index,
  })}
  removeClippedSubviews
  maxToRenderPerBatch={2}
  windowSize={5}
/>

The row component uses a plain Image with the CDN-resized URL and no fade:

const Thumb = ({uri}) => (
  <Image
    source={{ uri: `${uri}?w=240&h=180` }}
    style={styles.thumb}
    fadeDuration={0}
  />
);

We do not reach for `react-native-fast-image` here. It adds a native dependency and its own cache layer, and for forty cached thumbnails the gain in our testing has been marginal — a frame or two. The CDN resize plus the FlatList tuning above solves the problem without the extra library.

A 2GB phone holding somewhere in the mid-40s FPS through the full list is below 60, but it is smooth enough that warehouse staff stop reporting that scrolling stalls halfway down. That is the bar that matters for a list they use forty times a day.

←Back to all articles

Ready to build something great?

We help ambitious teams build software that lasts. If you're interested in working with us or want to discuss your project, let's connect.

Get in touch