skip to content

Why does a 2 MB JPEG in a React Native Image need more memory than its file size, and why do feeds suffer?

level: middleimportance: should knowfreq 42%

answer

  1. file size is compressed
  2. decoded bitmap: width x height x 4
  3. 12 megapixels, about 48 MB
  4. outside the JavaScript heap
  5. request images near display size

basics

~20 s

An Image is decoded into an uncompressed bitmap of roughly width x height x 4 bytes, so a 4000x3000 photo needs about 48 MB whatever its file size. A feed holding several at once exhausts memory, especially on low-end Android.

solid answer

~50 s

The JPEG's 2 MB is compressed; to draw it, the platform decodes it into a bitmap of about four bytes per pixel. A 12-megapixel photo, 4000 by 3000, becomes roughly 48 MB — twenty-odd times its file size — and it costs that even if the view is a 300-point card. In a travel feed several such images are decoded at once, and image caches keep recent ones around, so memory climbs by hundreds of megabytes. That memory lives in the native image pipeline, not the JavaScript heap, so a JavaScript heap snapshot looks fine while the process grows until a low-end Android phone kills it. The fix is to request images close to the size they are shown at — the view's size times `PixelRatio.get()` — typically as server-side thumbnails, and keep full resolution for a detail screen.

go deeper

for a junior

Remember that images are decoded to about four bytes per pixel, so a large photo costs tens of megabytes in memory regardless of its file size.

for a middle

Explain the arithmetic, why the view size does not limit the decode by itself, and why the memory is invisible in the JavaScript heap.

for a senior

Show how you would size image requests from layout and pixel ratio, keep originals for detail views, and verify the fix on a low-end Android device.

for a principal

Discuss where resizing belongs across client, API and image service, and how image memory budgets per screen feed into device-support decisions.

## File size versus decoded size Image files are **compressed**. A JPEG or WebP stores a clever encoding of the picture, and a busy 12-megapixel travel photo might be 2 MB on disk. To put pixels on screen, the platform must **decode** that file into an uncompressed **bitmap**: a grid holding a colour value for every pixel, commonly **4 bytes per pixel** (red, green, blue, alpha). The cost therefore depends on **pixel dimensions**, not file size: | Source image | Pixels | Decoded at 4 bytes per pixel | |---|---|---| | 1080 x 720 thumbnail | about 0.8 million | about 3 MB | | 2000 x 1500 | 3 million | about 12 MB | | 4000 x 3000 original | 12 million | about 48 MB | A 2 MB file and a 6 MB file of the same 4000 x 3000 photo decode to the same 48 MB. ## Why the view size does not save you A React Native `<Image>` shown in a 300 x 200 point card on a phone with a pixel ratio of 3 only needs 900 x 600 physical pixels — about 2 MB decoded. If the source is the 4000 x 3000 original, the platform holds far more pixels than the card can show unless the image is downsampled during decode. On Android, `Image` applies a heuristic (its `resizeMethod` prop, default `auto`) that may downsample, but it is a heuristic, and iOS has its own pipeline. The reliable fix is not to send the pixels in the first place. ## Why it matters in a feed A travel feed multiplies the cost: - **Several images are visible or near-visible at once**, and list components render some rows beyond the viewport. - **Image caches keep recently decoded images** so scrolling back is instant, which is good for speed and expensive for memory. - **Scrolling fast** decodes many images in a short time, producing peaks well above the steady state. - **The memory is outside the JavaScript heap.** Decoded bitmaps belong to the native image pipeline, so `performance.memory.usedJSHeapSize` and a JavaScript heap snapshot stay flat while the process grows. On a phone with plenty of RAM this may only cause stutter. On a **low-end Android** device with a small per-app budget, the process runs out: an allocation fails with an out-of-memory error, or the system kills the app in the background, often after some minutes of scrolling rather than immediately. ## Estimating a feed's peak Back-of-the-envelope numbers make the risk obvious in an interview: - **Originals:** fifteen feed images mounted at once — visible rows plus rows rendered ahead — at 48 MB each is about **720 MB** of bitmaps. - **Thumbnails:** the same fifteen images at 900 x 600 pixels are about 2 MB each, roughly **30 MB** in total. - **Caches** add recently scrolled images on top of either figure. The first number is beyond what a low-end Android phone will give one app; the second is comfortable almost anywhere. The difference comes entirely from pixel count. ## What to do instead 1. **Ask the server for the right size.** Compute the needed pixel width as layout width times `PixelRatio.get()` (or use `PixelRatio.getPixelSizeForLayoutSize`) and request a thumbnail of that size from an image CDN or your API. 2. **Keep originals for detail views.** Load the full-resolution image only when the user opens a single photo, and let it go when they leave. 3. **Prefer modern, compact formats** for transfer; this reduces download time, though decoded size stays tied to pixel count. 4. **Keep lists windowed**, so rows far from the viewport are unmounted and their images can be released. 5. **Measure on a low-end device**, where the problem appears first. ## Common mistakes - Judging memory by the file size or by the download size in the network panel. - Assuming a small `style` width and height shrinks the decoded bitmap by itself. - Looking only at the JavaScript heap for an image memory problem. - Fixing the symptom with a larger heap limit instead of smaller images. - Testing only on a recent flagship phone, where hundreds of megabytes of bitmaps may fit and the problem never shows. - Loading the original image for the feed "so the detail screen opens instantly" — prefetch a thumbnail-sized image for the feed and fetch the original only on demand.

  • How do you work out the pixel size to request for a React Native image card?
    Multiply the card's layout size in points by the device pixel ratio — `PixelRatio.get()` — or call `PixelRatio.getPixelSizeForLayoutSize(width)`. A 300-point-wide card on a device with pixel ratio 3 needs about 900 pixels of width, so request a thumbnail near that instead of the original.
  • Why can a JavaScript heap snapshot miss a React Native memory problem caused by images?
    Decoded bitmaps are held by the native image pipeline, not by Hermes. The JavaScript heap only contains the small objects describing the images, such as their URIs, so it can stay flat while process memory climbs. Use a native memory profiler, such as Android Studio's Memory Profiler, to see that growth.

A compressed photo is like a flat-packed wardrobe: it arrives in a small box, but once assembled it takes the same floor space whatever the box size. Decoding is the assembly, and a feed is a room where you assemble several at once.

saying these in an interview costs you the question

  • A 2 MB JPEG uses about 2 MB of memory when displayed
  • Style width and height alone guarantee a small decoded bitmap on every platform
  • Image memory shows up in the JavaScript heap snapshot
  • Raising the Android heap limit fixes image out-of-memory crashes
  • Converting to WebP reduces the decoded memory per pixel