Mobile — App Performance Optimization

Image Optimization in React Native (WebP, Caching)

Direct answer

Image optimization in React Native comes down to three controls: serve WebP instead of PNG or JPEG to cut transfer size meaningfully at equal quality, request images at the dimensions you actually render rather than decoding full-size originals, and cache aggressively with a library like expo-image using a memory-plus-disk policy so images load once per install, not once per mount. Decoded image memory scales with pixel dimensions — roughly four bytes per pixel — so sizing discipline matters even more than format choice.

Images are the heaviest thing most apps render, and they punish you three ways at once: network transfer, decode time on the CPU, and decoded bitmaps sitting in memory. This is the image pipeline I set up on client apps, ordered by how much each step typically saves.

Key facts, with sources

  • The median crash-free session rate across mobile apps in 2025 was 99.95%, with top-performing teams at the 75th percentile holding 99.99%. (Luciq Mobile App Stability Outlook 2025)
  • Compared with JavaScriptCore, the Hermes engine cuts typical React Native app startup from about 4.5 seconds to about 2.0 seconds, memory from about 185 MB to 136 MB, and engine app-size overhead from about 12 MB to 8 MB. (OneUptime Blog)
  • Real-world production migrations to React Native's New Architecture report around 43% faster cold starts, 39% faster rendering, and 26% lower memory usage. (RapidNative)
  • Shopify's performance bar for its React Native apps is critical screens loading in under 500 milliseconds at the 75th percentile, alongside over 99.9% crash-free sessions. (Shopify Engineering)
  • Published optimization case studies include Discord cutting app startup time in half and Coinbase reporting an 80% funnel performance improvement after its React Native rewrite. (CatDoes)

Understand what an image actually costs

The file size you see in your CDN dashboard is the smallest of an image's three costs. After transfer, the device decodes the compressed file into a raw bitmap, and that bitmap occupies memory proportional to pixel dimensions — roughly four bytes per pixel regardless of how well the file was compressed. A large camera-resolution photo decodes into a bitmap consuming tens of megabytes, even if the JPEG on the wire was small.

Decode work also happens on shared resources, so scrolling a feed that decodes oversized images produces jank that looks like a list-virtualization problem but is not. When I audit an app with mysterious scroll stutter and climbing memory, the image pipeline is one of the first places I look, and it is very often the answer.

Serve WebP, and let the client negotiate

WebP typically compresses noticeably smaller than JPEG at comparable visual quality and supports transparency without PNG's size penalty, which makes it my default recommendation for app imagery. Modern iOS and Android versions decode it, and the mainstream RN image libraries handle it on both platforms — though on bare Android with the core Image component you may need the optional decoder dependencies enabled.

The cleanest implementation puts format selection on the server or CDN: the client requests an image, headers or URL parameters signal supported formats, and the edge serves WebP to devices that accept it. This gives you a single migration point instead of an app release, and lets you adopt newer formats later by changing only edge configuration. If you control the upload pipeline, transcode once at ingest rather than paying per-request.

Request the size you render

The most common image mistake I find in audits is dimensional: a grid of small thumbnails, each backed by the full-resolution original. Every cell downloads a large file and decodes it into a bitmap dozens of times larger than the pixels on screen — memory balloons, scroll stutters, and low-end devices start dropping images or crashing under memory pressure.

The fix is resizing at the source: a CDN or image service that accepts width parameters and returns appropriately sized variants, with the app requesting dimensions derived from the layout and screen scale. As a rule I ask for the rendered size at the device's pixel ratio and nothing more. For fixed local layouts, generate the small variants at build time; the app should essentially never decode pixels it will not display.

Cache with expo-image and design for reuse

Re-downloading an avatar every time a screen mounts is pure waste, and users perceive it as flicker. I standardize on expo-image on current projects: its memory-and-disk cache policy means an image fetched once is served from memory while warm and from disk across sessions, and its recyclingKey prop prevents stale-image flashes when list cells are recycled. Blurhash placeholders and short transitions make loading feel deliberate instead of broken.

One caching subtlety costs teams real bandwidth: cache keys derive from the URL, so signed URLs whose tokens rotate defeat the cache silently — every mount is a miss with fresh query parameters. Where security allows, keep the cacheable part of the URL stable or configure the cache key explicitly, and confirm hits in a proxy before trusting the setup.

expo-image configured for a list cell
import { Image } from 'expo-image';

function AvatarCell({ item }: { item: Profile }) {
  return (
    <Image
      source={{ uri: item.thumbUri }}
      placeholder={{ blurhash: item.blurhash }}
      contentFit="cover"
      transition={150}
      cachePolicy="memory-disk"
      recyclingKey={item.id}
      style={{ width: 96, height: 96, borderRadius: 48 }}
    />
  );
}

Image-heavy lists need their own rules

Feeds and galleries multiply every image mistake by the number of cells, so they get extra constraints in my projects. Give every image an explicit width and height so layout never reflows when pixels arrive. Use a recycling list component so off-screen cells release their views. Keep cell images small — thumbnail variants in the list, with the full-resolution asset loaded only on the detail screen, ideally prefetched during the transition so the detail view appears instantly.

Prefetching deserves restraint: warming the next screen's hero image is a win; speculatively downloading fifty feed images burns the user's data and battery for content they may never scroll to. I prefetch only what the next user action makes near-certain, and I test the whole pipeline on a throttled connection, because that is where placeholder, cache, and sizing decisions become visible.

When to hire senior help

Bring in a senior performance specialist when your crash-free rate sits below roughly 99.9%, startup exceeds a couple of seconds on mid-range Android, or the team lacks profiling experience with Hermes, the JS thread, and native tooling. Performance rescue work is diagnostic in nature, so a short expert engagement typically finds the handful of hot spots that in-house teams spend months guessing at. If your stack includes React Native + Python + AI, a senior engineer who owns the full product beats coordinating multiple juniors.

Bottom line

Dhairya Senjaliya ships Mobile — App Performance Optimization projects worldwide — book a scoping call to discuss your specific situation.

Common pitfalls to avoid

  • Profiling only on high-end iPhones in dev mode, then discovering the real user base on low-end Android devices experiences multi-second startups and dropped frames
  • Rendering long feeds with ScrollView or an untuned FlatList instead of a virtualized list like FlashList, blocking the JS thread during scroll
  • Passing new inline functions and object literals on every render without memoization, causing cascading re-renders that never show up until lists grow
  • Ignoring JS bundle size and startup path, shipping no lazy loading or inline requires, so time-to-interactive balloons as the app grows

Frequently asked questions

Does React Native support WebP images?

Yes. Modern iOS and Android system decoders handle WebP, and the widely used image libraries support it on both platforms. On bare Android with the core Image component, animated or older WebP variants may require enabling optional Fresco decoder dependencies in your build. The practical approach is serving WebP from your CDN via content negotiation so capable clients get the smaller format automatically.

How does image caching work in React Native?

Libraries like expo-image cache at two levels: a memory cache for instant reuse while the app is running, and a disk cache that persists across launches, keyed by the image URL. Configure a memory-plus-disk policy for content images, and watch out for rotating signed URLs — changing query tokens create new cache keys, silently turning every load into a network fetch.

Why do images cause React Native apps to use so much memory?

Compressed file size is misleading: once decoded for display, an image occupies memory proportional to its pixel dimensions, roughly four bytes per pixel. Loading full-resolution photos into small thumbnail slots decodes bitmaps far larger than the screen needs, and a list of such cells can consume enormous memory. Requesting correctly sized variants from a CDN is the fix, ahead of any format change.

Which performance metrics should we actually track?

Crash-free session rate (the 2025 median is 99.95%, and below 99.8% is a red flag), cold start time, time-to-interactive, P75 screen-load time, and frame rate during scrolling. These need real-user monitoring in production, not just lab measurements.

Why is our React Native app slow on Android but fine on iOS?

The usual causes are testing only on flagship devices, JS-thread-blocking work, unvirtualized lists, and running an old React Native architecture or the JSC engine. Hermes and New Architecture migrations show measured gains of roughly 26 to 43% on memory and cold start, so upgrading the foundation is often the highest-leverage fix.

Does performance really affect revenue or is it an engineering vanity metric?

It compounds directly into acquisition and retention: crashes and slowness drive uninstalls and one-star reviews, and lower ratings measurably cut store conversion rates. That is why top consumer apps hold themselves to 99.99% crash-free sessions and sub-500ms screen loads.

Bottom line: Dhairya Senjaliya ships Mobile — App Performance Optimization projects worldwide. Book a scoping call at https://dhairyasenjaliya.com/#book-call.

Sources

Related guides

Keep up with new guides

New deep-dive guides on React Native, Python, and AI ship regularly. Subscribe via RSS or follow on LinkedIn.

Want help implementing this?

30-minute scoping call · Clear milestones · Senior engineer ownership