Skip to content

iOS OOM crash on lower-RAM devices: Image.compress() decodes at full resolution before resizing #414

Description

@ceres-quangd

Environment

  • react-native-compressor version: 2.0.2 (same root cause present since at least 1.16.0)
  • Platform: iOS (reproduced), Android has a related but narrower issue (see below)
  • Devices: iPhone 11, iPhone 13 (both 4GB RAM) — reproduced via Xcode Instruments Allocations/VM Tracker
  • React Native: 0.83.6, Expo SDK 55

Description

Image.compress() on iOS crashes the app (OOM / jetsam kill) when compressing several photos concurrently on lower-RAM devices, even though each individual photo is well within normal camera resolution (12MP HEIC).

Root cause

Both manualCompressHandler and autoCompressHandler in ImageCompressor.swift decode the source image at full native resolution before any downsampling happens:

  1. loadImage()UIImage(contentsOfFile:) / UIImage(data:) — decodes the full image.
  2. scaleAndRotateImage() — draws that full-resolution image into a second full-resolution CGContext just to apply EXIF orientation:
    guard let context = CGContext(data: nil, width: Int(image.size.width), height: Int(image.size.height), ...)
    context.draw(cgImage, in: ...)
  3. Only then does manualResize() / the inline resize in autoCompressHandler shrink the image down to maxWidth/maxHeight.

For a 12MP HEIC photo, step 2 alone needs roughly two ~49MB RGBA buffers in memory simultaneously (source + redraw target) — before the requested resize has done anything. An app that compresses a small batch of photos concurrently (e.g. an upload queue of 5) can hit iOS's jetsam memory limit purely from this decode/rotate step on 4GB-RAM devices, well before the resize logic runs.

Notably, getImageMetaData in the same module already avoids this exact problem for a different code path, with this comment:

// Use CGImageSourceCreateWithURL to avoid loading entire file into memory (prevents OOM crashes for large images)

...it just isn't applied to the actual compress path.

Android

manualCompressImage() in ImageCompressor.kt has the analogous issue — loadImage() uses BitmapFactory.decodeFile() with no downsampling, unlike autoCompressImage(), which already downsamples correctly via BitmapFactory.Options.inSampleSize.

Reproduction

  1. Pick 5+ HEIC photos from the camera roll on an iPhone with 4GB RAM (11, 13, 13 mini).
  2. Call Image.compress() on all of them concurrently (compressionMethod: 'manual' or 'auto', doesn't matter — both hit the same decode path).
  3. Watch memory in Xcode Instruments (Allocations / VM Tracker) — peak RSS spikes well past what the resulting compressed images would ever need, and the app is terminated by the OS on 4GB devices.

Suggested fix

Decode via CGImageSourceCreateThumbnailAtIndex with kCGImageSourceThumbnailMaxPixelSize + kCGImageSourceCreateThumbnailWithTransform: true, which downsamples and applies EXIF orientation in a single native decode step, without ever materializing the full-resolution bitmap. On Android, reuse the inSampleSize downsampling autoCompressImage() already does, in manualCompressImage() too.

I've verified this fix compiles cleanly on both platforms and resolves the crash in a downstream app (patched via patch-package, then confirmed with real device testing). Opening a PR with the fix now — will link it here.

While fixing this I also found and fixed a separate latent bug in copyExifInfo() that causes double-rotation once this decode path is used with a source that has a non-.up EXIF orientation. Details in the PR description.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions