If you've ever tried compressing videos in a React Native app, you've probably hit the same wall: FFmpeg. It works, but it adds ~9 MB to your APK, requires complex command-line arguments, and runs entirely in software — burning through battery and taking forever on longer videos.
There's a better way. Modern phones ship with dedicated hardware encoders (MediaCodec on Android, VideoToolbox on iOS) that can compress video 50x faster than FFmpeg while using a fraction of the battery. The problem? Accessing them from React Native has historically required writing native code.
Not anymore.
In this tutorial, I'll show you how to compress videos (and images) in Expo using hardware acceleration — no FFmpeg, no ejecting, no native code. Whether you're building a chat app, social media platform, or any app that handles user-generated media — this approach will save you bandwidth, storage, and headaches.
What You'll Learn
- How to compress videos with H.264 and HEVC hardware encoding
- How to compress images with quality and size control
- How to add progress bars and cancellation to your compression flow
- How to handle background compression on iOS
- A production-ready upload flow you can copy into your app
The Problem with FFmpeg in Mobile Apps
Before we dive in, let's understand why FFmpeg is a poor fit for mobile:
| Issue | Impact |
|---|---|
| APK size | Adds ~9 MB to your binary |
| Speed | Software encoding is 10-50x slower than hardware |
| Battery | CPU-intensive, drains battery fast |
| API complexity | You're writing shell commands as strings |
| No progress tracking | Hard to show users a progress bar |
Hardware encoders solve all of these. They're built into every phone's SoC, they're fast, and they're power-efficient. The challenge is just accessing them from JavaScript — which is what expo-image-and-video-compressor does.
Prerequisites
- React Native project with Expo SDK 51+
- Basic familiarity with TypeScript/JavaScript
- A physical device or emulator for testing (compression uses hardware encoders)
Setup
Install the package:
npx expo install expo-image-and-video-compressor
That's it. No config plugins, no expo prebuild, no ejecting. It works in both managed and bare Expo workflows because it's built as a true Expo Module.
Coming from
react-native-compressor? Unlikereact-native-compressor, this package doesn't require config plugins orexpo-dev-client. It works out of the box withexpo install.
Step 1: Basic Video Compression
Let's start with the simplest case — compress a video to a reasonable size for uploading:
import { compress } from 'expo-image-and-video-compressor';
async function compressVideo(uri: string) {
const compressedUri = await compress(uri, {
maxSize: 1080,
bitrate: 2_500_000,
codec: 'h264',
speed: 'ultrafast',
});
return compressedUri;
}
What's happening here:
-
maxSize: 1080— scales the video so the largest dimension is 1080px (aspect ratio preserved) -
bitrate: 2_500_000— targets 2.5 Mbps (good quality for social/chat apps) -
codec: 'h264'— uses H.264, which every device supports -
speed: 'ultrafast'— prioritizes encoding speed
A 4K, 30-second video (~85 MB) typically compresses to ~8 MB in about 3 seconds. That's hardware acceleration at work.
Step 2: Adding a Progress Bar
Users hate staring at a spinner. Let's add real-time progress:
import { useState } from 'react';
import { View, Text, Button } from 'react-native';
import { compress } from 'expo-image-and-video-compressor';
function VideoUploader({ videoUri }: { videoUri: string }) {
const [progress, setProgress] = useState(0);
const [compressing, setCompressing] = useState(false);
async function handleCompress() {
setCompressing(true);
setProgress(0);
const result = await compress(videoUri, {
maxSize: 720,
bitrate: 2_000_000,
codec: 'h264',
speed: 'fast',
}, (p) => {
setProgress(Math.round(p * 100));
});
setCompressing(false);
console.log('Compressed file:', result);
// Upload `result` to your server
}
return (
<View style={{ padding: 20 }}>
<Button title="Compress Video" onPress={handleCompress} disabled={compressing} />
{compressing && (
<View style={{ marginTop: 10 }}>
<Text>Compressing: {progress}%</Text>
<View style={{
height: 8,
backgroundColor: '#e0e0e0',
borderRadius: 4,
marginTop: 8,
}}>
<View style={{
height: 8,
backgroundColor: '#4CAF50',
borderRadius: 4,
width: `${progress}%`,
}} />
</View>
</View>
)}
</View>
);
}
The third argument to compress() is a callback that fires with a value from 0.0 to 1.0 as the encoder processes each frame. We multiply by 100 to get a percentage.
Step 3: Letting Users Cancel
Large videos can take a while. Give users an escape hatch:
import { compress, cancel } from 'expo-image-and-video-compressor';
let cancellationId: string;
// Start compression
const promise = compress(videoUri, {
maxSize: 1080,
getCancellationId: (id) => {
cancellationId = id;
},
}, (progress) => {
console.log(`${Math.round(progress * 100)}%`);
});
// Cancel button handler
function onCancelPress() {
if (cancellationId) {
cancel(cancellationId);
}
}
// Handle the result
try {
const result = await promise;
console.log('Compressed:', result);
} catch (error) {
console.log('Compression cancelled or failed');
}
When you call cancel(), the promise returned by compress() will reject, so always wrap it in a try/catch.
Step 4: Using HEVC for Even Smaller Files
HEVC (H.265) produces ~40% smaller files at the same visual quality compared to H.264. If you're building a chat app or social platform where storage and bandwidth costs matter, this is a big deal:
const result = await compress(videoUri, {
codec: 'hevc', // ~40% smaller than h264 at same quality
maxSize: 1080,
speed: 'fast',
});
Compatibility: HEVC encoding requires iOS 11+ and Android 5.0+ (API 21+), which covers virtually every device in active use today.
When to use which codec:
| Codec | File Size | Compatibility | Best For |
|---|---|---|---|
h264 |
Baseline | Universal | Maximum device support |
hevc |
~40% smaller | iOS 11+, Android 5+ | Chat apps, social platforms, bandwidth savings |
Step 5: Image Compression
The same package handles images too — no need for a separate library:
import { compressImage } from 'expo-image-and-video-compressor';
const result = await compressImage(imageUri, {
maxWidth: 1080,
maxHeight: 1080,
quality: 0.8, // JPEG quality (0.0 - 1.0)
output: 'jpg', // or 'png'
});
It handles orientation correction and EXIF preservation automatically — no more rotated photos after compression. If the compressed image is somehow larger than the original, it returns the original URI instead.
Step 6: Getting Video Metadata
Before compressing, you might want to check if compression is even needed:
import { getMetadata } from 'expo-image-and-video-compressor';
const meta = await getMetadata(videoUri);
console.log(meta);
// {
// width: 1920,
// height: 1080,
// duration: 30.5, // seconds
// size: 89400000, // bytes
// bitrate: 23000000, // bps
// extension: 'mp4'
// }
const sizeMB = meta.size / 1_000_000;
if (sizeMB < 5) {
console.log('Video is already small enough, skip compression');
}
Putting It All Together: Production Upload Flow
Here's a realistic example combining everything — metadata check, conditional compression, background support, and upload:
import {
compress,
compressImage,
getMetadata,
activateBackgroundTask,
deactivateBackgroundTask,
} from 'expo-image-and-video-compressor';
async function prepareAndUpload(
uri: string,
type: 'video' | 'image',
onProgress?: (p: number) => void,
) {
let compressedUri = uri;
if (type === 'video') {
// Check if compression is even needed
const meta = await getMetadata(uri);
const sizeMB = meta.size / 1_000_000;
console.log(`Original: ${sizeMB.toFixed(1)} MB, ${meta.width}x${meta.height}`);
if (sizeMB > 5) {
// Keep compression alive in background (iOS)
await activateBackgroundTask(() => {
console.warn('Background time expiring');
});
compressedUri = await compress(uri, {
maxSize: 720,
bitrate: 2_000_000,
codec: 'hevc',
speed: 'fast',
}, onProgress);
await deactivateBackgroundTask();
const newMeta = await getMetadata(compressedUri);
console.log(`Compressed: ${(newMeta.size / 1_000_000).toFixed(1)} MB`);
}
} else {
compressedUri = await compressImage(uri, {
maxWidth: 1080,
maxHeight: 1080,
quality: 0.75,
});
}
// Upload compressedUri to your API
await uploadToServer(compressedUri);
}
Key details:
-
getMetadata()— check the file size before deciding to compress -
activateBackgroundTask()— prevents iOS from killing your app mid-compression if the user switches apps - HEVC + 720p — a great default for chat/social uploads (small files, good quality)
- Skip small files — if the video is already under 5 MB, don't waste time compressing
Speed Presets Explained
The speed option controls the trade-off between encoding speed and compression ratio:
| Preset | Speed | File Size | Best For |
|---|---|---|---|
ultrafast |
Fastest | Larger | Quick previews, real-time UX |
fast |
Moderate | Medium | General purpose uploads |
balanced |
Slowest | Smallest | Storage-sensitive apps, background processing |
For most apps, ultrafast or fast is the right choice. The file size difference between presets is typically 10-20%, while the speed difference can be 3-5x.
Common Recipes
Chat App (WhatsApp-style)
await compress(videoUri, {
maxSize: 720,
bitrate: 1_500_000,
codec: 'hevc',
speed: 'fast',
});
Social Media Post
await compress(videoUri, {
maxSize: 1080,
bitrate: 3_000_000,
codec: 'h264',
speed: 'ultrafast',
});
Profile Photo
await compressImage(imageUri, {
maxWidth: 500,
maxHeight: 500,
quality: 0.85,
});
Thumbnail Generation
await compressImage(imageUri, {
maxWidth: 200,
maxHeight: 200,
quality: 0.6,
});
Wrapping Up
You don't need FFmpeg to compress videos in React Native. Hardware encoders exist on every phone — you just need a way to access them from JavaScript.
expo-image-and-video-compressor gives you:
- H.264 and HEVC hardware encoding
- Image compression with EXIF preservation
- Real-time progress tracking and cancellation
- iOS background task support
- Zero native code setup in Expo managed workflow
- ~50 KB APK impact (vs FFmpeg's ~9 MB)
Install it and try it out:
npx expo install expo-image-and-video-compressor
- GitHub: github.com/faeizfurqan17/expo-image-and-video-compressor
- npm: npmjs.com/package/expo-image-and-video-compressor
If this helped you, give the repo a star — it helps other developers find it. Drop a comment below if you have questions or run into issues. I read every one.
Follow me for more React Native and Expo content.
Top comments (0)