If youโve ever built a media-heavy app, you know the struggle.
A user snaps a gorgeous 4K video or a 12MP selfie and โ boom โ your upload queue explodes, your backend times out, and someone tweets,
โYour app just crashed ๐ญโ
Between different camera APIs, resolutions, and codecs, file sizes can jump from a few KB to hundreds of MB.
And simply compressing once often isnโt enough.
๐ก The smarter fix:
Set strict upload limits (e.g., โค 2 MB for images, โค 20 MB for video) and iteratively compress each file until it fits the cap โ all on-device.
Add good UX (progress bars, cancel buttons, smart defaults) and youโll deliver silky-smooth uploads even on slow connections.
๐งญ Table of Contents
- Why Compression Matters
- Quick Reference: Library Compatibility
- React Native CLI Setup (Images & Video)
- Expo Setup
- Full Image Compression & Upload Example
- Full Video Compression & Upload Example
- Iterative Compression Pattern
- Permissions & Troubleshooting
- Production Tips
- Final Thoughts
๐ฏ Why Compression Matters
- Slow uploads & timeouts: mobile networks drop large requests.
- Data cost: users burn bandwidth fast.
- Server load: big files inflate storage and CDN bills.
- UX: decoding large media tanks memory and drains battery.
Goal: compress client-side until it fits the backendโs file-size threshold.
โก Quick Reference: Library Compatibility
| Library | Min React Native Version | Notes |
|---|---|---|
| react-native-video v6 | โฅ 0.68.2 (RN 0.73 uses iOS 13+) | iOS โฅ 13.0 required from 6.0.0-beta.8 |
| react-native-fs | โฅ 0.61 โ use @2.16.0+
|
Native file access & utilities |
| react-native-image-picker | Works โฅ 0.60 | Handles photos & videos |
| react-native-compressor | Works โฅ 0.65 | Local media compression |
โ๏ธ React Native CLI Setup (Images & Video)
Weโll use:
| Package | Purpose | Docs |
|---|---|---|
react-native-image-picker |
Pick or capture photos & videos | |
react-native-compressor |
Compress images/videos efficiently | |
react-native-fs |
File size utilities & uploads | |
react-native-video |
Full-control video player |
๐งฉ Installation
npx @react-native-community/cli@latest init MediaUploadRN
cd MediaUploadRN
npm install react-native-image-picker react-native-compressor react-native-fs react-native-video
cd ios && pod install && cd ..
โ ๏ธ react-native-fs Version Compatibility
| React Native Version | Install This Version |
|---|---|
| < 0.57 / Gradle < 3 | react-native-fs@2.11.17 |
| โฅ 0.57 / Gradle โฅ 3 | react-native-fs@2.13.2+ |
| โฅ 0.61 โ | react-native-fs@2.16.0+ |
npm install react-native-fs@2.16.0
# RN < 0.60 only:
npx react-native link react-native-fs
CocoaPods (iOS):
pod 'RNFS', :path => '../node_modules/react-native-fs'
pod install
๐ฌ React Native Video (v6.0.0+) Setup
โ ๏ธ V6.0.0 Information
- Version 6 introduces modern architecture & performance improvements.
- Requires React Native โฅ 0.68.2
- From 6.0.0-beta.8 โ requires iOS โฅ 13.0 (default in RN 0.73).
- For older RN projects, stick with
react-native-video@5.2.x.
๐ฆ Installation
npm install react-native-video
# or
yarn add react-native-video
๐ฑ iOS Setup
cd ios && pod install && cd ..
Minimum: iOS 13 | RN 0.68.2+
Enable Custom Features (Podfile)
Video Caching:
$RNVideoUseVideoCaching=true
Google IMA (ads):
$RNVideoUseGoogleIMA=true
If caching is enabled, also add to Gemfile:
gem "cocoapods-swift-modular-headers"
and run:
bundle install
bundle exec pod install
๐ค Android Setup
Your project must use Kotlin โฅ 1.8 and compileSdk 34:
buildscript {
ext.kotlinVersion = '1.8.0'
ext.compileSdkVersion = 34
ext.targetSdkVersion = 34
}
Optional ExoPlayer flags (android/build.gradle):
buildscript {
ext {
useExoplayerHls = true
useExoplayerDash = true
useExoplayerSmoothStreaming = true
useExoplayerIMA = false // enable if ads needed
useExoplayerRtsp = false
}
}
โ
Default enabled: SmoothStreaming, Dash, HLS
โ ๏ธ Each feature adds size โ enable only what you need.
๐ฑ Android Permissions
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<!-- For older SDKs -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
๐ iOS Permissions
<key>NSPhotoLibraryUsageDescription</key>
<string>We need access to your library to select media.</string>
<key>NSCameraUsageDescription</key>
<string>We need access to your camera to capture media.</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need access to your microphone for video audio.</string>
๐ผ React Native Cli Image Compression & Upload Example
import React, { useState } from "react";
import {
View,
Button,
Image,
Alert,
ScrollView,
StyleSheet,
} from "react-native";
import * as ImagePicker from "react-native-image-picker";
import { Image as ImageCompressor } from "react-native-compressor";
import RNFS from "react-native-fs";
const IMAGE_MAX_BYTES = 2 * 1024 * 1024; // 2 MB
const UPLOAD_URL = "https://your.api/uploads";
async function getFileSize(uri: string) {
const stat = await RNFS.stat(uri.replace("file://", ""));
return Number(stat.size || 0);
}
async function compressImageUntilUnder(uri: string, maxBytes: number) {
let quality = 0.8;
let currentUri = uri;
for (let i = 0; i < 5; i++) {
const compressed = await ImageCompressor.compress(currentUri, {
compressionMethod: "auto",
quality,
});
const size = await getFileSize(compressed);
if (size <= maxBytes) return compressed;
quality = Math.max(0.3, quality - 0.15);
currentUri = compressed;
}
return currentUri;
}
async function uploadFile(uri: string) {
const form = new FormData();
form.append("image", {
// @ts-ignore
uri,
name: "upload.jpg",
type: "image/jpeg",
});
await fetch(UPLOAD_URL, { method: "POST", body: form });
}
export default function ImageUploader() {
const [uri, setUri] = useState<string | null>(null);
const [compressedUri, setCompressedUri] = useState<string | null>(null);
const pickImage = async () => {
const res = await ImagePicker.launchImageLibrary({ mediaType: "photo" });
if (res.didCancel || !res.assets) return;
setUri(res.assets[0].uri ?? null);
};
const handleUpload = async () => {
if (!uri) return Alert.alert("Pick an image first");
const compressed = await compressImageUntilUnder(uri, IMAGE_MAX_BYTES);
setCompressedUri(compressed);
await uploadFile(compressed);
Alert.alert("Upload complete!");
};
return (
<ScrollView contentContainerStyle={styles.container}>
<Button title="Pick Image" onPress={pickImage} />
{uri && <Image source={{ uri }} style={styles.img} />}
<Button title="Compress & Upload" onPress={handleUpload} />
{compressedUri && (
<Image source={{ uri: compressedUri }} style={styles.img} />
)}
</ScrollView>
);
}
const styles = StyleSheet.create({
container: { padding: 20, gap: 16 },
img: { height: 250, resizeMode: "contain", backgroundColor: "#eee" },
});
๐ฅ React Native Cli Video Compression & Upload Example
import React, { useState } from "react";
import { View, Button, Text, Alert, StyleSheet } from "react-native";
import * as ImagePicker from "react-native-image-picker";
import { Video as VideoCompressor } from "react-native-compressor";
import RNFS from "react-native-fs";
import Video from "react-native-video";
const VIDEO_MAX_BYTES = 20 * 1024 * 1024; // 20 MB
const UPLOAD_URL = "https://your.api/uploads";
async function getFileSize(uri: string) {
const stat = await RNFS.stat(uri.replace("file://", ""));
return Number(stat.size || 0);
}
async function compressVideoUntilUnder(
uri: string,
maxBytes: number,
onProgress?: (p: number) => void
) {
const presets: Array<"auto" | "medium" | "low"> = ["auto", "medium", "low"];
let currentUri = uri;
for (const preset of presets) {
const compressed = await VideoCompressor.compress(
currentUri,
{ compressionMethod: preset },
(p) => onProgress?.(Math.round(p * 100))
);
const size = await getFileSize(compressed);
if (size <= maxBytes) return compressed;
currentUri = compressed;
}
return currentUri;
}
async function uploadVideo(uri: string) {
const form = new FormData();
form.append("video", {
// @ts-ignore
uri,
name: "video.mp4",
type: "video/mp4",
});
await fetch(UPLOAD_URL, { method: "POST", body: form });
}
export default function VideoUploader() {
const [uri, setUri] = useState<string | null>(null);
const [compressedUri, setCompressedUri] = useState<string | null>(null);
const [progress, setProgress] = useState<number | null>(null);
const pickVideo = async () => {
const res = await ImagePicker.launchImageLibrary({ mediaType: "video" });
if (res.didCancel || !res.assets) return;
setUri(res.assets[0].uri ?? null);
};
const handleUpload = async () => {
if (!uri) return Alert.alert("Pick a video first");
const compressed = await compressVideoUntilUnder(
uri,
VIDEO_MAX_BYTES,
setProgress
);
setCompressedUri(compressed);
await uploadVideo(compressed);
Alert.alert("Upload complete!");
setProgress(null);
};
return (
<View style={styles.container}>
<Button title="Pick Video" onPress={pickVideo} />
{progress != null && <Text>Compressing: {progress}%</Text>}
<Button
title="Compress & Upload"
onPress={handleUpload}
disabled={progress != null}
/>
{compressedUri && (
<Video
source={{ uri: compressedUri }}
style={styles.video}
controls
paused
/>
)}
</View>
);
}
const styles = StyleSheet.create({
container: { padding: 20, gap: 16 },
video: { height: 220, backgroundColor: "#000" },
});
โ๏ธ Expo Setup โ Capture, Compress & Upload Media
Expo makes working with media surprisingly straightforward โ whether youโre handling simple image uploads or full video workflows.
You can choose between two reliable approaches depending on your project type:
-
๐ผ Image Compression Only (Pure Expo) โ works entirely inside the Managed Workflow using
expo-image-manipulator. -
๐ฅ Video Compression (Dev Build Mode) โ unlocks native-level compression and playback with
react-native-compressor,react-native-fs, andreact-native-video.
Both methods deliver efficient, on-device compression โ no backend preprocessing required.
๐ผ Image Compression Only โ Expo Image Manipulator
If youโre building with Pure Expo (Managed Workflow), you can compress and resize images directly on the device using expo-image-manipulator.
This library is fast, stable, and ideal for reducing image size before uploads โ perfect for profile photos, product images, or gallery uploads.
โ๏ธ Installation
npx expo install expo-image-manipulator expo-file-system expo-image-picker
๐ป Example โ Dynamic Image Compression & Resizing
The example below dynamically compresses any selected image to stay under a custom file-size limit (e.g., 2 MB, 5 MB, 10 MB).
It progressively reduces quality and resolution until the image meets the target threshold โ all on-device.
import React, { useEffect, useState } from "react";
import {
View,
Text,
StyleSheet,
Button,
ActivityIndicator,
Image,
Alert,
} from "react-native";
import * as ImagePicker from "expo-image-picker";
import * as FileSystem from "expo-file-system";
import { useImageManipulator, SaveFormat } from "expo-image-manipulator";
const DEFAULT_COMPRESSION_QUALITY = 0.8;
const MIN_QUALITY = 0.3;
const DOWNSCALE_FACTOR = 0.85;
// ---------- File Size Utilities ----------
const getFileSize = async (uri: string): Promise<number> => {
const info = await FileSystem.getInfoAsync(uri);
return info.size || 0;
};
const formatFileSize = (bytes: number) =>
`${(bytes / 1024 / 1024).toFixed(2)} MB`;
// ---------- Dynamic Compression Logic ----------
const compressImageUntilUnder = async (
uri: string,
manipulator: ReturnType<typeof useImageManipulator>,
maxSizeMB: number
): Promise<string> => {
try {
const MAX_FILE_SIZE = maxSizeMB * 1024 * 1024;
let quality = DEFAULT_COMPRESSION_QUALITY;
let currentUri = uri;
let currentSize = await getFileSize(uri);
let width: number | undefined;
let height: number | undefined;
console.log(
`๐ธ Starting compression: ${formatFileSize(
currentSize
)} (limit ${maxSizeMB} MB)`
);
while (currentSize > MAX_FILE_SIZE && quality > MIN_QUALITY) {
if (width || height) manipulator.resize({ width, height });
const rendered = await manipulator.renderAsync();
const compressed = await rendered.saveAsync({
compress: quality,
format: SaveFormat.JPEG,
});
const newSize = await getFileSize(compressed.uri);
console.log(`โ ${formatFileSize(newSize)} @ quality ${quality}`);
if (newSize > MAX_FILE_SIZE) {
quality *= 0.8;
width = (rendered.width || 0) * DOWNSCALE_FACTOR;
height = (rendered.height || 0) * DOWNSCALE_FACTOR;
currentUri = compressed.uri;
currentSize = newSize;
} else {
console.log(`โ
Final size under ${maxSizeMB} MB`);
return compressed.uri;
}
}
console.warn(
`โ ๏ธ Could not compress below ${maxSizeMB} MB โ returning best result.`
);
return currentUri;
} catch (err) {
console.error("Compression error:", err);
return uri;
}
};
// ---------- Compression Overlay ----------
const ImageCompressor = ({
uri,
onComplete,
maxSizeMB,
}: {
uri: string;
onComplete: (resultUri: string) => void;
maxSizeMB: number;
}) => {
const manipulator = useImageManipulator(uri);
const [isCompressing, setIsCompressing] = useState(false);
useEffect(() => {
const compress = async () => {
setIsCompressing(true);
try {
const originalSize = await getFileSize(uri);
console.log("Original size:", formatFileSize(originalSize));
const rendered = await manipulator.renderAsync();
const temp = await rendered.saveAsync({
compress: DEFAULT_COMPRESSION_QUALITY,
format: SaveFormat.JPEG,
});
const firstPassSize = await getFileSize(temp.uri);
let finalUri = temp.uri;
if (firstPassSize > maxSizeMB * 1024 * 1024) {
finalUri = await compressImageUntilUnder(
temp.uri,
manipulator,
maxSizeMB
);
}
const finalSize = await getFileSize(finalUri);
console.log("โ
Final compressed size:", formatFileSize(finalSize));
onComplete(finalUri);
} catch (e) {
console.error(e);
onComplete(uri);
} finally {
setIsCompressing(false);
}
};
compress();
}, [uri]);
if (!isCompressing) return null;
return (
<View style={styles.overlay}>
<ActivityIndicator size="large" color="#0F6B2D" />
<Text style={styles.overlayText}>Compressing image...</Text>
</View>
);
};
// ---------- Main Component ----------
export default function ExpoImageCompressor() {
const [imageUri, setImageUri] = useState<string | null>(null);
const [finalUri, setFinalUri] = useState<string | null>(null);
const [maxSizeMB, setMaxSizeMB] = useState<number>(2); // dynamic user limit
const pickImage = async () => {
const res = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
quality: 1,
});
if (!res.canceled) {
setImageUri(res.assets[0].uri);
}
};
return (
<View style={styles.container}>
<Button title="Pick Image" onPress={pickImage} />
{imageUri && <Image source={{ uri: imageUri }} style={styles.img} />}
<Text style={styles.label}>Max Size: {maxSizeMB} MB</Text>
{imageUri && (
<ImageCompressor
uri={imageUri}
maxSizeMB={maxSizeMB}
onComplete={(uri) => {
setFinalUri(uri);
Alert.alert("โ
Compression complete!");
}}
/>
)}
{finalUri && <Image source={{ uri: finalUri }} style={styles.img} />}
</View>
);
}
// ---------- Styles ----------
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 20,
},
img: { width: "90%", height: 250, marginTop: 20, borderRadius: 12 },
label: { marginTop: 16, fontSize: 16, color: "#333" },
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: "rgba(0,0,0,0.7)",
justifyContent: "center",
alignItems: "center",
},
overlayText: { color: "white", marginTop: 12 },
});
๐ง Why It Works
-
Dynamic Limit: Easily change the size cap (
maxSizeMB) for different use cases (e.g., 2 MB for avatars, 10 MB for photos). - Iterative Compression: Reduces both quality and resolution in steps until under the defined limit.
- Expo-Only: Runs fully in JavaScript โ no native linking or prebuild required.
- Real-World Safe: Handles large images gracefully and provides user feedback with an activity overlay.
-
Reusable: Drop into any project โ just pass a
maxSizeMBvalue and a completion callback.
๐ฅ Video Compression โ Dev Build Mode
If your app needs to handle video uploads or large media files, switch to an Expo Dev Build.
This mode enables access to native libraries for true on-device compression, caching, and playback.
โ๏ธ Installation
npm install react-native-compressor react-native-fs react-native-video
npx expo prebuild
npx expo run:android
# or
npx expo run:ios
Once complete, your project will have native access to efficient compression and smooth video rendering.
๐ป Example โ Dynamic Video Compression Helper
import { Video as VideoCompressor } from "react-native-compressor";
import RNFS from "react-native-fs";
/**
* Compresses a video dynamically until it fits under the given size (in MB).
* Supports both Expo Dev Build and React Native CLI setups.
*/
export async function compressVideoUntilUnder(
uri: string,
maxSizeMB: number,
onProgress?: (p: number) => void
): Promise<string> {
try {
const maxBytes = maxSizeMB * 1024 * 1024;
const presets: Array<"auto" | "medium" | "low"> = ["auto", "medium", "low"];
let currentUri = uri;
const getFileSize = async (fileUri: string): Promise<number> => {
const stat = await RNFS.stat(fileUri.replace("file://", ""));
return Number(stat.size || 0);
};
let currentSize = await getFileSize(currentUri);
console.log(
`๐ฌ Original size: ${(currentSize / 1024 / 1024).toFixed(2)} MB`
);
for (const preset of presets) {
console.log(`โ๏ธ Trying preset: ${preset}`);
const compressed = await VideoCompressor.compress(
currentUri,
{ compressionMethod: preset },
(progress) => onProgress?.(Math.round(progress * 100))
);
const newSize = await getFileSize(compressed);
console.log(`โ ${formatSize(newSize)} after ${preset}`);
if (newSize <= maxBytes) {
console.log(`โ
Final size under ${maxSizeMB} MB`);
return compressed;
}
currentUri = compressed;
currentSize = newSize;
}
console.warn(
`โ ๏ธ Could not compress below ${maxSizeMB} MB โ returning best result.`
);
return currentUri;
} catch (err) {
console.error("โ Compression failed:", err);
return uri;
}
}
const formatSize = (bytes: number) => `${(bytes / 1024 / 1024).toFixed(2)} MB`;
โ What You Unlock
| Library | Purpose |
|---|---|
| react-native-compressor | Efficient on-device video and image compression |
| react-native-fs | File-size inspection, local reads/writes, and uploads |
| react-native-video | Smooth playback, caching, and adaptive streaming |
๐ Note: Expo Dev Builds fully support config plugins, allowing features like caching and streaming exactly like a bare React Native project.
๐งฉ Summary
-
๐ผ Pure Expo (Managed Workflow): Use
expo-image-manipulatorfor image compression โ 100% JavaScript, no native build needed. -
๐ฅ Dev Build Mode (Native): Use
react-native-compressorandreact-native-videofor professional-grade video compression and playback.
Both approaches help you deliver faster uploads, smaller files, and better mobile UX โ all while staying true to Expoโs developer-friendly workflow.
๐ Iterative Compression Pattern
while (size(uri) > limit && attempts < MAX_ATTEMPTS) {
uri = compress(uri, nextParams());
}
Ensures consistent output sizes across devices.
๐งฐ Permissions & Troubleshooting
-
Android 13+:
READ_MEDIA_IMAGES,READ_MEDIA_VIDEO -
Older Android:
READ_EXTERNAL_STORAGE -
iOS:
NSCameraUsageDescription,NSPhotoLibraryUsageDescription,NSMicrophoneUsageDescription - Always check file size after compression โ Android
content://URIs can differ. - Test on real devices; emulators lack cameras and true codecs.
๐ Production Tips
- Enforce size limits in the backend too.
- Resize before compressing (e.g., 1920 px width cap).
- Adjust compression by connection type (Wi-Fi vs cellular).
- Generate thumbnails for video feeds.
- Keep compression async โ never block the UI.
โ Final Thoughts
Compression isnโt just a backend optimization โ itโs a user-experience feature.
By iteratively compressing files, tracking progress, and configuring each library correctly (especially React Native Video v6 and react-native-fs), youโll keep uploads fast, consistent, and reliable across every device.
๐ฌ If this guide helped, drop a โค๏ธ or share your own setup for React Native media uploads.
Top comments (0)