Skip to main content
RapidDev - Software Development Agency
App Featuresforms-data18 min read

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

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).

4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members

Feature spec

Intermediate

Category

forms-data

Build with AI

1-2 days with FlutterFlow + custom Dart actions

Custom build

1-2 weeks custom dev

Running cost

$0/mo up to 1K users; $25-75/mo at 10K users

Works on

Mobile

Everything it takes to ship File Compression and Optimization — parts, prompts, and real costs.

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.

What users consider table stakes in 2026

  • Automatic compression before upload with no required manual step — the user selects the file and compression happens transparently in the background
  • Size-before/after comparison shown after compression completes: '2.4 MB → 340 KB — saved 86%' in an animated card
  • Upload progress bar that covers both the compression phase and the upload phase with distinct labels
  • Support for JPEG, PNG, WebP, and HEIC — HEIC is the default format on iPhones and must be handled without requiring users to convert first
  • No visible quality degradation at the default compression level (85% quality for photos, 90% for documents with text)
  • Graceful handling of already-small files — if the source file is under 100KB, skip compression and upload as-is

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.

Layers:UIDataBackendService

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.

Note: On Android, HEIC decoding is not supported natively — flutter_image_compress throws FormatException when given a HEIC file on an Android device. Detect the file extension or MIME type before compression and route Android HEIC files to a server-side handler.

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.

Note: video_compress has a known bug on iOS 17 where the progress stream stalls at 100% but the output file is not yet written. The workaround is to use AVAssetExportSession directly via a platform channel, or switch to the ffmpeg_kit_flutter package which has more predictable iOS behavior.

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%.

Note: pdf-lib is Deno-compatible but has limited image resampling capabilities — it removes duplicate objects and unused resources but cannot re-encode embedded JPEG images at a lower quality level. For aggressive PDF compression, a server with Ghostscript or a paid PDF API (ILovePDF, Smallpdf API) is needed.

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.

Note: For uploads over 10MB, implement a Flutter foreground service using flutter_foreground_task to keep the upload running when the app goes to background. iOS suspends background tasks aggressively — without a foreground service, large uploads fail when the user switches apps.

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.

Note: Only show the size reporter if the compressed file is actually smaller — if compression inflates the file (common with small PNGs re-encoded as JPEG), show 'Original quality preserved' instead.

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.

Note: SharedPreferences values survive app restarts but not app uninstalls. If quality preferences need to sync across devices, store them in the user's Supabase profile row instead.

Build it — pick your path

Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.

Hand-built by developersFit for this feature:

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.

Step by step

  1. 1Add 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. 2Implement 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. 3Add 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. 4Implement 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. 5Add 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

Where this path bites

  • 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

Third-party services you'll need

Compression runs entirely on-device using free open-source libraries. The only variable cost is Supabase Storage for storing the compressed files.

ServiceWhat it doesFree tierPaid from
flutter_image_compress (pub.dev)On-device image compression for JPEG, PNG, WebP, and HEIC before uploadFully free (Apache 2.0)Free, open-source
video_compress (pub.dev)On-device video compression and H.264 re-encoding with progress streamingFully free (MIT)Free, open-source
pdf-lib (Deno)Server-side PDF compression in Supabase Edge Functions — strips fonts, metadata, and duplicate objectsFully free (MIT)Free, open-source
Supabase StorageResumable upload API for chunked mobile uploads; stores compressed files1GB Storage, 2GB egress freePro $25/mo (100GB Storage); egress $0.09/GB above free tier (approx)

Swipe the table sideways to see pricing.

What it costs to run

Drag through the tiers to see how your monthly bill scales with users — no surprises later.

Estimated monthly running cost

$0/mo

Compression is entirely client-side and free. Supabase free tier (1GB Storage) comfortably handles compressed files at this scale — a typical compressed photo is 300–700KB, so 100 users uploading 10 photos each is about 700MB total.

Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.

What breaks when AI tools build this

The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.

HEIC files fail to compress on Android devices

Symptom: 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

Symptom: 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

Symptom: 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

Symptom: 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

1

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

2

Use compute() for all compression operations longer than 200ms — any image from a modern smartphone camera qualifies; never compress on the main isolate

3

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

4

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

5

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

6

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

7

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

When You Need Custom Development

FlutterFlow with custom Dart actions handles basic image compression well. These requirements go beyond what that approach can sustain:

  • App requires lossless compression — medical imaging, legal documents, and architectural drawings where any quality loss is unacceptable and lossy formats like JPEG are prohibited
  • Compliance requirement that compression must happen server-side — for certain regulated industries, the uncompressed file must never be accessed by client-side code before going through an approved processing pipeline
  • Batch compression of thousands of files with a progress dashboard, retry queue, and per-file status — the mobile foreground service pattern doesn't scale to large batch workflows
  • Integration with a CDN image transformation pipeline (Cloudflare Images, Imgix, Bunny.net) that handles all resizing and format conversion at delivery time, making client-side compression redundant

RapidDev builds these features for production

Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.

Get a fixed-price quote

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.

RapidDev

Need this feature production-ready?

RapidDev builds file compression and optimization into real apps — auth, database, payments — at $13K–$25K.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.