Client-Side Image Compression with JavaScript
You do not need a server to compress images. Modern browsers can do it entirely in JavaScript using the Canvas API. This is how tools like MiniPx work โ and you can build something similar yourself.
The basic approach
Client-side image compression follows three steps: load the image into a Canvas element, optionally resize it, then export it with a quality parameter. The browser's native image encoder handles the heavy lifting.
Here is the simplest working implementation:
function compressImage(file, quality = 0.7) {
return new Promise((resolve) => {
const img = new Image();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
img.onload = () => {
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
canvas.toBlob(
(blob) => resolve(blob),
'image/jpeg',
quality
);
};
img.src = URL.createObjectURL(file);
});
}This function takes a File object (from an input element or drag-and-drop) and returns a compressed Blob. The quality parameter ranges from 0 (maximum compression, worst quality) to 1 (minimum compression, best quality).
Adding resize support
Resizing is often more effective than quality reduction for shrinking file sizes. A 4000x3000 photo resized to 1920x1440 drops about 75% of its pixels before compression even kicks in.
function compressAndResize(file, maxWidth, quality = 0.7) {
return new Promise((resolve) => {
const img = new Image();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
img.onload = () => {
let width = img.width;
let height = img.height;
if (width > maxWidth) {
height = Math.round((height * maxWidth) / width);
width = maxWidth;
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(
(blob) => resolve(blob),
'image/jpeg',
quality
);
};
img.src = URL.createObjectURL(file);
});
}Targeting a specific file size
Sometimes you need a file under a specific size โ 100 KB for a form upload, or 1 MB for an email attachment. The trick is to compress iteratively, reducing quality until you hit the target.
async function compressToSize(file, targetBytes, maxWidth) {
const img = await loadImage(file);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Resize first
let { width, height } = calculateDimensions(
img.width, img.height, maxWidth
);
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
// Binary search for the right quality
let lo = 0.1, hi = 0.95, best = null;
while (hi - lo > 0.02) {
const mid = (lo + hi) / 2;
const blob = await canvasToBlob(canvas, 'image/jpeg', mid);
if (blob.size <= targetBytes) {
best = blob;
lo = mid; // Try higher quality
} else {
hi = mid; // Need lower quality
}
}
return best;
}This binary search approach typically finds the optimal quality in 5-7 iterations. Each iteration takes just a few milliseconds because the image is already on the canvas โ only the encoding step repeats.
Format options: JPEG vs WebP
The Canvas API supports exporting to different formats. JPEG is universally supported and great for photos. WebP produces 25-35% smaller files at the same visual quality.
// JPEG output canvas.toBlob(callback, 'image/jpeg', 0.8); // WebP output (smaller files, modern browsers) canvas.toBlob(callback, 'image/webp', 0.8); // PNG output (lossless, no quality param) canvas.toBlob(callback, 'image/png');
Check browser support before using WebP โ it works in all modern browsers but not in older versions of Safari (pre-14). You can feature-detect it by creating a small WebP canvas and checking if the blob is valid.
Handling the compressed output
Once you have the compressed Blob, you can trigger a download, display a preview, or upload it to your server:
// Trigger download
function downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
// Show preview
function previewBlob(blob, imgElement) {
imgElement.src = URL.createObjectURL(blob);
}
// Upload to server
async function uploadBlob(blob, filename) {
const formData = new FormData();
formData.append('image', blob, filename);
await fetch('/api/upload', {
method: 'POST',
body: formData
});
}Performance tips
Use OffscreenCanvas for batch processing. If you are compressing multiple images, OffscreenCanvas can run in a Web Worker, keeping the UI responsive. This is how MiniPx handles batch compression without freezing the page.
Clean up object URLs. Every call to URL.createObjectURL() allocates memory that is not automatically freed. Always call URL.revokeObjectURL() when you are done with the URL, or you will leak memory โ especially in batch scenarios.
Resize before compressing. Drawing a 12-megapixel image at full resolution onto a canvas and then exporting it at quality 0.3 produces worse results than resizing to 2 megapixels and exporting at quality 0.8. Resize first, compress second.
What the Canvas API cannot do
The browser's built-in encoder is good but not optimal. Dedicated tools like MozJPEG or libavif produce 10-20% smaller files at the same quality. To use these in the browser, you need WebAssembly builds โ which is what Squoosh does.
For most applications, the native Canvas API is good enough. The convenience of zero dependencies and instant processing outweighs the marginal size improvement from custom codecs.
Frequently asked questions
Related tools
Compress, convert, and resize images in your browser. Nothing gets uploaded.
Open MiniPx โ