I had 200 product photos to compress for a client's e-commerce site. The Photoshop batch processor worked, but launching a 2 GB application to run one command felt like using a flamethrower to light a candle. I wanted something that ran directly in the browser — no installs, no uploads to some random server.
How browser image compression actually works
There are three real approaches:
| Approach | How it works | Upside | Downside |
|---|---|---|---|
| Canvas API | Draw image to <canvas>, export with toBlob()
|
Zero dependencies, works everywhere | Limited to Canvas-supported formats |
| WebAssembly | Compile libjpeg-turbo or libwebp to WASM | Full control over compression params | Heavy bundle (~500 KB), complex setup |
| OffscreenCanvas + Workers | Do Canvas compression on background threads | Non-blocking UI, batch-friendly | Browser support still patchy |
I picked Canvas. For 90% of real-world cases — product photos, blog images, user uploads — it handles the job without adding a 500 KB WASM binary to your bundle.
The code
Here is the core function:
function compressImage(file, { quality = 0.7, maxWidth = 1920, format = 'image/webp' } = {}) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = reject;
reader.onload = (e) => {
const img = new Image();
img.onerror = reject;
img.onload = () => {
let { width, height } = img;
if (width > maxWidth) {
height = Math.round(height * (maxWidth / width));
width = maxWidth;
}
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(
(blob) => {
if (!blob) return reject(new Error('toBlob returned null'));
resolve(blob);
},
format,
quality
);
};
img.src = e.target.result;
};
reader.readAsDataURL(file);
});
}
Wiring it up to a file input:
const input = document.querySelector('#image-input');
input.addEventListener('change', async (e) => {
for (const file of e.target.files) {
const originalSize = file.size;
const compressed = await compressImage(file, { quality: 0.6, format: 'image/webp' });
console.log(
`${file.name}: ${(originalSize / 1024).toFixed(0)} KB → ${(compressed.size / 1024).toFixed(0)} KB ` +
`(${((1 - compressed.size / originalSize) * 100).toFixed(0)}% smaller)`
);
}
});
A bug I hit: the PNG default trap
canvas.toBlob(callback) without a format argument defaults to image/png. PNG is lossless — your "compressed" image ends up larger than the original JPEG. I spent 20 minutes wondering why my 2 MB file turned into a 4 MB blob.
Always pass the format explicitly. 'image/webp' is the right call in 2026 — every browser supports it now.
Batching without freezing
For multiple files, fire them all at once with Promise.all:
async function compressBatch(files, opts) {
const blobs = await Promise.all(
Array.from(files).map(f => compressImage(f, opts))
);
// blobs ready — download individually or pack into a zip
return blobs;
}
For 100+ images, move the canvas work into a Web Worker with OffscreenCanvas. The API looks almost identical but runs off the main thread. No more frozen tabs.
Real numbers
I grabbed 50 product photos from an e-commerce catalog (average 2.4 MB JPEG, 3000×3000 px):
| Quality | Format | Avg output | Reduction | Visual quality |
|---|---|---|---|---|
| 0.9 | JPEG | 310 KB | 87% | Same as original on screen |
| 0.7 | JPEG | 145 KB | 94% | Slightly softer edges at 200% zoom |
| 0.7 | WebP | 98 KB | 96% | Matches JPEG 0.9 to the naked eye |
| 0.5 | WebP | 62 KB | 97% | Fine for thumbnails, too aggressive for product pages |
Total time for 50 images: 4.2 seconds (Chrome 126, M1 Mac). That is under 85 ms per image.
For the e-commerce project, WebP at quality 0.7 was the clear winner. 96% smaller files, zero visible difference on a 27-inch display.
What the Canvas approach cannot do
PDF compression, background removal, and format conversion (PNG to SVG, etc.) all need different pipelines. You can build those with WebAssembly, but that belongs in a separate article.
What I took away from this
Browser image compression works better than most people assume. You do not need a server, a library, or Photoshop. Roughly 50 lines of JavaScript gets you 96% compression with no visible quality loss.
canvas.toBlob()without a format argument will burn you. Always pass'image/webp'or'image/jpeg'explicitly.Web Workers make batch processing genuinely usable on 50+ images without freezing the tab.
I put all of this into a tool at ComprimeFotos. The UI is in Spanish (I originally built it for a Spanish-speaking audience), but the tool itself needs no translation — drag an image, it compresses, you download. No signup, no ads, no uploads.
Top comments (0)