Device-Side Compression and Uploads

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

See the section bibliography for the reference material consulted while preparing these pages.

Baseline date: 2026-10-07. A phone camera produces files that are far larger than any screen needs: a 12 to 48 megapixel photo is several megabytes, and a minute of 4K video can be hundreds of megabytes. Sending the original over a mobile connection is slow, expensive for the user and fragile. Compressing on the device first helps, but the server still has the final word, see upload pipelines.

What to do on the device, and what not to

On the device On the server

Images

Downscale to the largest size the product needs (for example 2048 pixels on the long side), apply the EXIF rotation, and encode as JPEG or WebP at a sane quality. Convert HEIC to JPEG only if the backend cannot read HEIC.

Validate the file, strip metadata, produce all variants and formats, compute the BlurHash, see image formats.

Video

Optionally transcode to a moderate resolution and bit rate, to shorten the upload.

Always transcode to the ladder and package for streaming, see video encoding fundamentals.

Metadata and privacy

Strip location if the user did not intend to share it.

Strip it again. Never rely on the client for security.

Rules of thumb:

  • The server must never trust the client’s work: it validates and re-encodes everything. Device-side compression is an optimisation of the upload only.

  • Compress when the user is on mobile data or the file is big. A small, already compressed JPEG gains little, and each re-encode loses quality (generation loss).

  • Keep the original when the product needs it (for example a photo editor) and upload a compressed copy for previews.

  • Do not block the UI: compression takes seconds for images and minutes for video, and drains the battery.

HEIC and HEVC from iOS

iPhones capture HEIC photos and HEVC video by default. The files are small and not widely playable outside Apple platforms. Two options:

  • Ask iOS for a compatible format. Set the picker or the camera to export compatible formats (JPEG, H.264), at the cost of larger files. PHPickerConfiguration.preferredAssetRepresentationMode = .compatible and UIImagePickerController.videoExportPreset choose what is delivered. See Camera, Media and Photos and AVFoundation.

  • Upload HEIC and HEVC unchanged, and convert on the server. This keeps the smallest upload. The server needs a libheif and an HEVC-capable FFmpeg, which have licensing implications, see image formats and encoding and video encoding fundamentals.

Android phones record H.264 or HEVC, and many save images as JPEG, with HEIC and AVIF available on newer devices.

Compressing images

Platform Tool

Android

Decode with the size of the target (ImageDecoder with setTargetSize, or Coil and Glide with size), then Bitmap.compress(Bitmap.CompressFormat.WEBP_LOSSY, 80, out) or JPEG. Apply ExifInterface rotation first.

iOS

ImageIO thumbnails with CGImageSourceCreateThumbnailAtIndex (fast, downsampled at decode), then UIImage.jpegData(compressionQuality: 0.8) or the HEIC destination.

React Native, Expo

expo-image-manipulator (resize, rotate, compress to JPEG, PNG or WebP).

Flutter

flutter_image_compress, or the image package for pure Dart.

Use the ImageIO and ImageDecoder downsampling paths rather than decoding the full image and then scaling, which can run out of memory on a 48-megapixel photo. See the Bitmap section of Image loading with Coil and Glide.

Compressing video

Android: Media3 Transformer, see Media3. iOS: AVAssetExportSession presets, see AVFoundation. Cross-platform libraries wrap those (for example react-native-compressor). Choose H.264 at 720p as the target when you transcode on the device, which every server-side pipeline can read. Device encoders vary in quality, so treat their output as an upload format, not as a delivery format.

Uploading

Upload straight to object storage with a pre-signed URL from the API, as in upload pipelines. The app then tells the API that the upload is complete, or the storage event triggers processing.

Background uploads

A user will lock the phone or switch apps during a large upload. Use the platform’s background transfer service:

  • iOS. A background URLSession configuration, with uploadTask(with:fromFile:). The system continues the transfer after the app is suspended and relaunches it on completion, see AVFoundation for the code.

  • Android. WorkManager with a CoroutineWorker marked as a long-running foreground worker (setForeground), or DownloadManager for downloads. WorkManager retries with backoff and survives process death. Reference: WorkManager and long-running workers.

  • React Native and Expo. expo-file-system upload tasks with background support, or react-native-background-upload, both of which wrap the native services.

Resumable and multipart uploads

For files of tens of megabytes or more, a dropped connection should not restart the upload from zero:

  • S3 multipart upload, Google Cloud Storage resumable uploads and Azure Blob block uploads split the file into parts that are uploaded independently and retried. The API signs one URL per part (S3) or returns a session URL (Google).

  • tus (tus.io) is an open protocol for resumable uploads with client libraries for the main platforms, for backends that do not use object storage directly.

  • Track which parts finished so the app can resume after a restart, and show real progress from the part count.

Progress and failure

Show progress from the transfer callbacks, and the server-side processing state afterwards (processing, ready, failed) by polling or from a push notification, see upload pipelines. Retry transient failures with backoff, keep the compressed file until the API confirms the upload, and let the user cancel. Pre-signed URLs expire, so ask for a new one when a retry happens after the expiry.