
Compressly is a JavaScript library for resizing and compressing image files directly in the browser before upload.
It handles individual photos or batches, lets you control image dimensions and encoding quality, and returns File objects for your existing upload code.
The library can encode images in a Web Worker when browser capabilities permit, and an optional React hook exposes progress and results.
Features
- Proportional downscaling for oversized images.
- JPEG, WebP, and PNG output formats.
- Native image orientation handling for camera photos.
- Automatic pass-through for files that meet the size and dimension limits.
- Progress updates with processing stages.
- Concurrent batch jobs with per-file completion callbacks.
- Detailed compression results with file sizes, reduction percentages, and processing times.
How To Use It
Installation
Install Compressly through npm for vanilla JavaScript projects with a build tool or for framework applications.
npm install compressly
For a page that loads JavaScript modules directly from a CDN, use the browser module below.
<script type="module">
import { compressImage } from 'https://cdn.jsdelivr.net/npm/[email protected]/dist/index.js';
// Call compressImage() after obtaining a File from an input or drop zone.
</script>
Basic Usage
Use an image file input and pass its selected File to compressImage(). The promise resolves to a File, which can be uploaded, previewed, or downloaded.
<input id="photo-input" type="file" accept="image/*">
import { compressImage } from 'compressly';
const input = document.querySelector('#photo-input');
input.addEventListener('change', async () => {
const file = input.files?.[0];
if (!file) return;
const result = await compressImage(file, {
maxWidth: 1600,
maxHeight: 1600,
quality: 0.8,
});
console.log(`${file.size} bytes -> ${result.size} bytes`);
});
Compress Multiple Images
compressImages() takes a File[] and returns the processed files in their original order. A multi-file input supplies a FileList, which needs conversion to an array.
<input id="photo-batch" type="file" accept="image/*" multiple>
import { compressImages } from 'compressly';
const picker = document.querySelector('#photo-batch');
picker.addEventListener('change', async () => {
const files = Array.from(picker.files ?? []);
if (!files.length) return;
const results = await compressImages(files, {
concurrency: 2,
maxWidth: 1600,
onBatchProgress(percent, completed, total) {
console.log(`${completed}/${total} complete (${percent}%)`);
},
});
console.log(results);
});
Enforce an Upload Limit
targetSizeMB asks Compressly to reduce the encoded file below a specified size. Check result.file.size before uploading to an endpoint with a strict limit.
import { compressImageWithDetails } from 'compressly';
const input = document.querySelector('#photo-input');
input.addEventListener('change', async () => {
const file = input.files?.[0];
if (!file) return;
const limitBytes = 500 * 1024;
const result = await compressImageWithDetails(file, {
targetSizeMB: limitBytes / (1024 * 1024),
mimeType: 'image/jpeg',
});
if (result.file.size > limitBytes) {
console.error('The processed image exceeds the upload limit.');
return;
}
const form = new FormData();
form.append('photo', result.file);
await fetch('/api/photos', { method: 'POST', body: form });
});
Convert an Image to WebP
Small files that already meet the default dimension and file-size limits pass through unchanged. Set forceResize: true when an existing image must be re-encoded into a requested output format.
import { compressImage } from 'compressly';
async function convertToWebP(file) {
return compressImage(file, {
mimeType: 'image/webp',
forceResize: true,
quality: 0.8,
});
}
React Integration
React apps can use useImageCompressor() from compressly/react. This example receives a selected photo, starts compression, and displays the resulting file size. Use it in a client component in frameworks that render components on the server.
import { useImageCompressor } from 'compressly/react';
export function AvatarUploader() {
const { compress, isCompressing, progress, lastResult, error } =
useImageCompressor({ maxWidth: 640, maxHeight: 640 });
async function handleChange(event) {
const file = event.currentTarget.files?.[0];
if (!file) return;
try {
await compress(file);
} catch {
// Errors from rejected operations appear in the hook's error state.
}
}
return (
<div>
<input type="file" accept="image/*" onChange={handleChange} disabled={isCompressing} />
{isCompressing && <p>Compressing: {progress}%</p>}
{lastResult && <p>Processed size: {Math.round(lastResult.compressedSize / 1024)} KiB</p>}
{error && <p role="alert">{error.message}</p>}
</div>
);
}
All Configuration Options
maxSizeMB(number, default1): Size threshold in MiB. An eligible file at or below the threshold can pass through unchanged when it also meets the dimension limits.targetSizeMB(number, optional): Requests a maximum output size in MiB. Attempts up to four encodes, adjusting quality and sometimes dimensions. The limit is not guaranteed.maxWidth(number, default1920): Maximum width in pixels, with aspect ratio preserved.maxHeight(number, default1920): Maximum height in pixels, with aspect ratio preserved.quality(number, default0.82): Initial JPEG or WebP encoding quality, normally from0.1to1. PNG encoding does not use the quality value.mimeType('image/jpeg' | 'image/webp' | 'image/png', optional): Requests the output MIME type. PNG inputs use PNG by default. Other raster images use JPEG by default. Files skipped by the size check keep their original format.autoRotate(boolean, defaulttrue): Requests EXIF orientation handling during image decoding. Actual orientation handling also depends on the browser’s native image APIs.useWorker(boolean, defaulttrue): Attempts encoding in a Web Worker withOffscreenCanvaswhen available. Uses regular Canvas when Worker encoding cannot run.forceResize(boolean, defaultfalse): Forces raster image processing even when the input already meets the size and dimension limits.onProgress((progress: number, phase: CompressionPhase) => void, optional): Receives processing milestones from0to100and a compression phase. It is not a network upload progress callback.
Batch Configuration
BatchCompressOptions extends CompressOptions with three properties.
concurrency(number, default3): Number of concurrent image jobs. Supply a positive integer.onFileComplete((result: CompressResult, index: number, total: number) => void, optional): Called after each file finishes.indexuses the original zero-based file position.onBatchProgress((overallPercentage: number, completedCount: number, totalCount: number) => void, optional): Called after a file completes with the overall batch percentage and counts.
Compression Functions
The main compressly entry point exposes four compression functions. All accept browser File inputs. The regular variants return files, and the WithDetails variants return result objects.
compressImage(file: File, options?: CompressOptions): Promise<File> compressImageWithDetails(file: File, options?: CompressOptions): Promise<CompressResult> compressImages(files: File[], options?: BatchCompressOptions): Promise<File[]> compressImagesWithDetails(files: File[], options?: BatchCompressOptions): Promise<CompressResult[]>
Compression Result Reference
CompressResult contains these six public fields. An image that bypasses processing returns the original File. If the library cannot decode or compress a file, it can return the original file with reductionPercentage set to 0.
file(File): Returned image file.originalSize(number): Input size in bytes.compressedSize(number): Returned file size in bytes.reductionPercentage(number): Calculated percentage reduction, with negative results reported as0.timeTakenMs(number): Processing duration in milliseconds.dimensions({ width: number, height: number }): Width and height reported by the compression operation. Both are0when dimensions cannot be determined.
Progress Phases
CompressionPhase is a TypeScript union of idle, reading, orienting, resizing, compressing, and complete. The React hook uses idle as its initial state. The compression functions report milestones through onProgress.
Other Public Exports
In addition to the four compression functions, the main package exports three utility functions and the associated TypeScript types. Call close() on LoadedImageSource after working with a loaded image to release its resources.
calculateDimensions(currentWidth: number, currentHeight: number, maxWidth: number, maxHeight: number): Returns{ width: number, height: number }. It scales down images to the specified bounds and preserves the aspect ratio.loadImageWithExif(file: File, autoRotate: boolean = true): ReturnsPromise<LoadedImageSource>withsource,width,height, andclose(). Thesourceis anImageBitmapor anHTMLImageElement.isWorkerSupported(): Returns abooleanbased on the presence ofWorker,OffscreenCanvas, and related browser APIs. Security policies can prevent actual Worker execution even when it returnstrue.
React Hook API
useImageCompressor(defaultOptions?: CompressOptions) exports the following values from compressly/react. It tracks the current operation and previous results.
compress(file: File, overrideOptions?: CompressOptions): ReturnsPromise<CompressResult>for one image.compressMultiple(files: File[], overrideOptions?: BatchCompressOptions): ReturnsPromise<CompressResult[]>for a batch.isCompressing(boolean):trueduring an active hook operation.progress(number): Latest compression or batch percentage.phase(CompressionPhase): Current processing phase.lastResult(CompressResult | null): Most recent single-file result.lastBatchResults(CompressResult[]): Most recent batch result array.error(Error | null): Error captured when a hook operation rejects.reset(): Resets stored status, progress, results, and error. It does not cancel work already in progress.
Alternatives & Related Resources
- Privacy-First Client-side Image Compression & Resizing in JavaScript
- Client Side Image Compressor In JavaScript
FAQs
Q: Can I compress an image inside a Next.js Server Component?
A: Image encoding needs browser APIs. Call the compression function from a client-side event handler. The package can be imported in server-rendered projects, but a server-side call does not perform browser compression.
Q: How does Compressly handle animated GIFs and SVG files?
A: It returns the original GIF or SVG File without compression. Validate File.type if your upload flow accepts only formats you plan to re-encode.
Q: Why can the result be larger than the original file?
A: Re-encoding can increase file size, especially when a size target forces another pass. Compare result.file.size against the original size before choosing which file to upload.
Q: Why does image processing run on the main thread even with useWorker: true?
A: Web Worker encoding requires Worker, OffscreenCanvas, and permission to create a Blob-based worker. When that cannot run, Compressly uses Canvas on the main thread. A site’s Content Security Policy can prevent worker creation.







