# How to Add File Compression to Your Mobile App (Copy-Paste Prompts Included)

- Tool: App Features
- Last updated: July 2026

## TL;DR

File compression runs client-side on the device before upload: flutter_image_compress handles images (JPEG/PNG/WebP/HEIC), video_compress handles video, and a Supabase Edge Function using pdf-lib handles PDFs. With FlutterFlow you can ship basic image compression in 2–3 days using custom Dart actions. The libraries are free — costs only appear via Supabase Storage egress at scale ($0–25/mo at 1K users).

## What File Compression and Optimization Actually Is

File compression runs on the device before upload — not on the server after. The user selects an image, video, or PDF; the app runs a compression routine in the background; then uploads the smaller file to Supabase Storage. For a typical photo (4MB from an iPhone camera), flutter_image_compress at 85% quality produces a 400–700KB output — an 80–90% size reduction with no visible quality loss at screen resolution. The visible product decision is how much to expose to the user: fully automatic with no UI (just upload the compressed file), a size-before/after comparison card, or a quality slider for power users. Getting the HEIC-to-JPEG conversion right on Android and keeping the UI thread unfrozen during compression are the two technical challenges that trip up most first builds.

## Anatomy of the Feature

Five components. Image compression is the most commonly needed piece and the only one FlutterFlow can partially handle visually. Video, PDF, and chunked upload all require custom Dart code.

- **Image compression layer** (service): Flutter's flutter_image_compress package compresses JPEG, PNG, WebP, and HEIC files to a target quality level (default 85) and maximum output dimensions (e.g., 1920×1080) before the file leaves the device. Converts HEIC files (iPhone default) to JPEG automatically on iOS. Runs on a background isolate via Flutter's compute() function to avoid blocking the UI thread.
- **Video compression** (service): The video_compress Flutter package reduces video bitrate and resolution before upload. Supports H.264 re-encoding. Exposes a progress stream that feeds a Flutter StreamBuilder driving the progress bar widget. Typical compression: 100MB raw video → 10–15MB at 720p/1Mbps with no visible quality loss for social or documentation use.
- **PDF compression** (backend): A Supabase Edge Function (Deno) using the pdf-lib library strips embedded fonts, downsamples images inside the PDF, and removes metadata. Called after the user selects a PDF file from the device — the raw PDF bytes are uploaded to a temporary Supabase Storage path, the Edge Function reads them, compresses, and returns compressed bytes which are then uploaded to the final destination path. PDF compression ratios vary widely: image-heavy PDFs compress 60–80%; text-only PDFs may compress only 5–10%.
- **Upload manager** (backend): The dio Flutter package handles chunked multipart upload to Supabase Storage with a 512KB chunk size appropriate for mobile networks. Retries on network error with exponential backoff (3 attempts: 1s, 2s, 4s delay). Uses Supabase Storage's resumable upload API to continue from the last successful chunk after a network interruption rather than restarting from zero. The upload progress feeds the same progress bar widget as the compression phase.
- **Size reporter UI** (ui): A Flutter widget that displays before/after file size as an animated card that slides up after compression completes. Reads the original file size from File.lengthSync() before compression and the compressed size from flutter_image_compress's callback Uint8List.length. Formats sizes using the intl package NumberFormat.compact() (e.g., '2.4 MB → 340 KB'). Calculates and displays the savings percentage.
- **Compression settings store** (data): Flutter SharedPreferences persists the user's preferred quality level (50/75/85/100) and maximum output dimension setting. No server-side storage needed — these preferences live locally on the device. If the app exposes a quality slider, read this value as the flutter_image_compress quality parameter. Default to 85 for photos and 90 for documents.

## Build paths

### Flutterflow — fit 3/10, 2-3 days

FlutterFlow can add flutter_image_compress via Settings and build the size reporter UI visually, but video compression, PDF compression, and the isolate setup for non-freezing compression all require custom Dart actions. The form-filling side is visual; the compression logic is code.

1. In FlutterFlow, go to Settings → Pubspec Dependencies and add flutter_image_compress and dio to the dependencies list
2. Add a custom Dart action called CompressImage: it accepts a File path as input, calls FlutterImageCompress.compressWithFile() with quality=85 and minWidth=1920, and returns the compressed Uint8List and the compressed file size as output variables
3. Build the file picker widget visually using a FilePicker action on a button, then wire the output to your CompressImage custom action
4. Add a second custom Dart action called UploadToSupabase: takes the compressed Uint8List and calls the Supabase Storage upload API via dio with chunked upload; wire it to run after CompressImage completes
5. Build the size reporter UI visually: a Card widget with two Text widgets (original size, compressed size) and an AnimatedContainer that slides in after the CompressImage action finishes
6. Enable FlutterFlow code export (Pro $70/mo) if you need to customize the compute() isolate call for background processing — this cannot be done through the visual editor

Limitations:

- video_compress and pdf-lib PDF compression cannot be configured through FlutterFlow's visual Action editor — both require full custom Dart code actions
- Running flutter_image_compress on the main isolate (the only option without code export) freezes the UI for 1–3 seconds on high-resolution photos from modern iPhone cameras
- HEIC-to-JPEG conversion on Android requires checking Platform.isAndroid before calling the compression function — this logic requires a custom Dart action
- FlutterFlow Pro ($70/mo) required for code export if you need to wrap compression in Flutter's compute() for background isolate execution

### Lovable — fit 1/10, not recommended

Lovable outputs web apps (Vite/React), not Flutter mobile apps. This feature is specifically tagged mobile. Browser-based image compression via browser-image-compression is possible for web, but lacks HEIC support and has no video or PDF compression capability.

1. For a web equivalent of image compression (not this mobile feature), add browser-image-compression npm package to a Lovable project
2. Use imageCompression(file, { maxSizeMB: 0.5, maxWidthOrHeight: 1920 }) before uploading to Supabase Storage
3. Note that HEIC files uploaded from iPhone in Safari mobile require server-side conversion — browser-image-compression cannot decode HEIC in the browser

Starter prompt:

```
Build a web image compression tool. Use the browser-image-compression npm package to compress files client-side before uploading to Supabase Storage: call imageCompression(file, { maxSizeMB: 0.5, maxWidthOrHeight: 1920, useWebWorker: true }) so compression runs off the main thread. Show original and compressed file sizes side-by-side ('2.4 MB → 340 KB, saved 86%'). Compare sizes before committing — if compressedFile.size >= originalFile.size, upload the original. Add a quality slider (50–100, default 85). For HEIC files (file.type === 'image/heic'), route to a Supabase Edge Function that uses sharp to convert to JPEG before returning the binary. Upload the final file to Supabase Storage under the user's folder. Show upload progress via Supabase's onUploadProgress callback. Handle states: idle, compressing, uploading with percentage, done with download link, and error with retry button.
```

Limitations:

- Not a mobile build path — Lovable does not produce Flutter/native mobile apps
- browser-image-compression lacks HEIC support — iPhone users uploading default-format photos get errors
- No video compression without a server-side FFmpeg function — not achievable in the browser
- Use FlutterFlow or custom Flutter development for this feature

### Custom — fit 5/10, 1-2 weeks

Full Flutter implementation with flutter_image_compress + video_compress + Supabase resumable uploads + Dart isolates. The correct path for apps where upload quality and size are core product metrics — photo sharing, document management, field service apps.

1. Add flutter_image_compress, video_compress, dio, and flutter_foreground_task to pubspec.yaml; for PDF compression, set up a Supabase Edge Function with pdf-lib
2. Implement the image compression function using compute() to run flutter_image_compress in a background isolate: return a CompressResult object containing the compressed Uint8List and the compressed byte count
3. Add the size guard: compare compressedBytes.length to originalBytes.length; if compressed is larger, return the original file unchanged with a flag indicating no compression was applied
4. Implement chunked upload using dio's FormData multipart with Supabase Storage's resumable upload TUS protocol endpoint; handle network interruptions by storing the upload offset in SharedPreferences and resuming from that offset on retry
5. Add flutter_foreground_task for uploads over 10MB: start the foreground service before the upload begins, show a persistent notification with upload progress, stop the service when the upload completes or fails

Limitations:

- HEIC conversion on Android requires flutter_image_compress 1.1.3+ and still has edge cases on older Android versions — test on a real Android device, not just the emulator
- video_compress has a known iOS 17 progress stall bug — the AVAssetExportSession workaround adds platform-specific code that complicates the implementation

## Gotchas

- **HEIC files fail to compress on Android devices** — flutter_image_compress converts HEIC files on iOS natively because iOS has built-in HEIC codec support. Android devices lack a system HEIC decoder, so the package throws a FormatException when given a HEIC file on Android. The error occurs silently in some FlutterFlow builds — the upload proceeds with the uncompressed original, which may be 10x larger than expected. Fix: Before calling flutter_image_compress, check `Platform.isAndroid` and inspect the file extension or MIME type. If the file is HEIC on Android, either display 'This format is not supported on Android — please use JPEG or PNG' with a camera re-capture option, or upload the raw HEIC file to a Supabase Edge Function that converts it server-side using the sharp Node.js library. The server-side path adds latency but handles all devices correctly.
- **UI freezes for 1-3 seconds during compression on high-resolution photos** — Calling flutter_image_compress directly in a button tap handler (or any synchronous UI callback) runs on the main isolate. Processing a 4K photo from a modern iPhone camera takes 1–3 seconds of CPU time, during which Flutter cannot render frames. The user sees the app freeze with no loading indicator — it appears crashed. Fix: Wrap the compression call inside Flutter's compute() function, which runs the operation in a background isolate and returns the result to the main thread. The API is: `final compressed = await compute(compressImage, imageFile)` where compressImage is a top-level function (not a closure or method) that takes the file path and returns the compressed Uint8List. Show a loading indicator before calling compute() and remove it in the .then() callback.
- **Compression inflates small PNG files** — A 50KB PNG screenshot re-encoded as JPEG at 85% quality often produces a 180–250KB output — the opposite of the goal. This happens because JPEG is lossy and adds its own encoding overhead on files that were efficiently compressed as lossless PNG. The size reporter shows a negative saving, which confuses users. Fix: Always compare the compressed output size to the original before committing: `if (compressedBytes.length >= originalBytes.length) { return originalFile; }`. Add this guard in the custom Dart action before returning the result. Show 'Original quality preserved — file was already optimal' in the size reporter UI instead of a negative savings percentage.
- **Supabase resumable upload session fails when app backgrounds on iOS** — When the user starts a large upload (video or high-resolution photo) and then switches to another app, iOS suspends the Flutter isolate running the upload. The Supabase resumable upload TUS session has a server-side inactivity timeout (60 seconds by default). When the user returns to the app, the session has expired and the upload must restart from the beginning — losing all upload progress. Fix: For uploads over 10MB, start a flutter_foreground_task foreground service before the upload begins. The foreground service keeps the app process alive on iOS with a persistent notification showing upload progress. On iOS, request background refresh entitlement and BGTaskScheduler registration. Store the last successful chunk offset in SharedPreferences so if the session does expire, re-query the TUS endpoint for the current offset before restarting rather than uploading from byte 0.

## Best practices

- Always run the size comparison guard — if compressed output is larger than the original, upload the original file unchanged and log the skip reason in the upload metadata
- Use compute() for all compression operations longer than 200ms — any image from a modern smartphone camera qualifies; never compress on the main isolate
- Set sensible defaults (quality: 85, maxWidth: 1920) and only expose a quality slider if your product's users have explicitly asked for control — most users prefer automatic
- Show a combined compression-and-upload progress bar with two labeled phases rather than two separate indicators — 'Compressing... 100% — Uploading... 34%' is less confusing than two bars
- Handle the HEIC-on-Android case explicitly with a user-facing message rather than a silent failure — a cryptic error after a long wait is worse than an early clear instruction
- Store compression metadata (original_size, compressed_size, compression_ratio, format) in the parent resource table as a JSONB column — this data is valuable for product analytics and debugging user complaints
- Test compression on real devices, not just emulators — emulators often have faster CPU and unlimited memory that masks the UI freeze and memory pressure issues that appear on real hardware

## Frequently asked questions

### How do I compress images before uploading to Supabase Storage in a Flutter app?

Add flutter_image_compress to pubspec.yaml. Before calling the Supabase upload, run: `final compressed = await FlutterImageCompress.compressWithFile(file.path, quality: 85, minWidth: 1920, minHeight: 1080)`. The result is a Uint8List of the compressed image bytes. Wrap this in compute() to keep the UI thread unfrozen. Then create a XFile from the bytes and upload to Supabase Storage using the supabase_flutter client. Compare compressed.length to the original file size before uploading — if compressed is larger, upload the original instead.

### Which Flutter package is best for image compression?

flutter_image_compress is the standard choice — it uses platform-native codecs (libjpeg on Android, ImageIO on iOS) which are faster and more memory-efficient than pure-Dart alternatives. It handles JPEG, PNG, WebP, and HEIC formats and runs synchronously (wrap in compute() for background execution). For web Flutter targets, flutter_image_compress has limited support — use the browser-image-compression npm package via a JS interop layer instead.

### How do I handle HEIC files from iPhones in a Flutter app?

On iOS, flutter_image_compress converts HEIC to JPEG automatically — no extra configuration needed. On Android, HEIC is not natively supported: the package throws FormatException. Before compressing, check `Platform.isAndroid` and the file extension. For Android HEIC files, either prompt the user to retake the photo in JPEG mode, or upload the raw file to a Supabase Edge Function that uses the sharp library (Node.js) to convert HEIC to JPEG server-side. The server-side conversion adds 2–5 seconds of latency but works on all devices.

### Can I compress video before uploading from a mobile app?

Yes — use the video_compress Flutter package. Call `MediaInfo? info = await VideoCompress.compressVideo(file.path, quality: VideoQuality.MediumQuality, deleteOrigin: false)`. The package returns a MediaInfo object with the output path and file size. Attach a progress listener via VideoCompress.compressProgress$ stream and feed it to a StreamBuilder for the progress bar. Note the iOS 17 progress stall bug — if your app targets iOS 17+, test on a real device before shipping and use the AVAssetExportSession workaround if the progress stream stops at 100%.

### How do I show a progress bar during file compression and upload?

Split the progress display into two phases. For compression: flutter_image_compress doesn't expose a progress stream — show an indeterminate spinner labeled 'Compressing...' then switch to determinate progress when compression finishes. For upload: use dio's onSendProgress callback `(int sent, int total) { setState(() { uploadProgress = sent / total; }); }` to drive a LinearProgressIndicator. The combined UI pattern is: compression spinner → 'Compressing complete' checkmark → upload progress bar with percentage → 'Upload complete' success state.

### What compression quality level should I use to balance size and visual quality?

Quality 85 is the standard for photos — it produces 70–90% file size reduction with no visible quality loss at screen resolution. Use quality 90 for documents with text, diagrams, or fine detail where lower quality introduces visible JPEG artifacts on text edges. Never go below 60 for user-facing photos — artifacts become visible and users complain. For thumbnail previews that are displayed at 64×64 or 128×128 pixels, you can use quality 70 since the small display size masks compression artifacts.

### How do I prevent the UI from freezing during image compression?

Wrap the compression call in Flutter's compute() function: `final compressed = await compute(_compressImage, filePath)` where `_compressImage` is a top-level function (not a method or closure — compute() requires a top-level function for Dart isolate serialization). Show a loading overlay before calling compute() and dismiss it in the .then() callback. The compute() function spawns a separate Dart isolate so the main thread continues rendering at 60fps during compression. Without compute(), a 12MP iPhone photo freezes the UI for 1–3 seconds.

---

Source: https://www.rapidevelopers.com/app-features/file-compression-and-optimization
© RapidDev — https://www.rapidevelopers.com/app-features/file-compression-and-optimization
