DEV Community

Li Yi
Li Yi

Posted on

Compress images in the browser — 20 lines, zero uploads

The boring problem

Almost every "compress your image" site does the same thing: your file leaves the browser, gets uploaded to a bucket, is processed by something on the other end, and comes back as a download link. Fine for a wallpaper. Less fine for a screenshot with a customer's address in it, an ID photo, or a draft design that hasn't shipped.

Browsers have had a perfectly good image encoder the whole time. Here it is.

The whole thing

async function compress(file, quality = 0.8) {
  const bitmap = await createImageBitmap(file); // applies EXIF orientation

  const canvas = document.createElement('canvas');
  canvas.width = bitmap.width;
  canvas.height = bitmap.height;
  const ctx = canvas.getContext('2d');

  // JPEG has no alpha channel: transparent areas would come out black.
  ctx.fillStyle = '#ffffff';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  ctx.drawImage(bitmap, 0, 0);
  bitmap.close();

  return new Promise((resolve) =>
    canvas.toBlob(resolve, 'image/jpeg', quality)
  );
}
Enter fullscreen mode Exit fullscreen mode

canvas.toBlob(blob, type, quality) is the actual encoder. The third argument only applies to lossy formats. That is the entire pipeline — decode, resample, encode — and no byte of it touches a network.

Four details that separate this from a toy

1. EXIF orientation. Phone photos carry an Orientation tag. If you decode via new Image() and drawImage, many browsers ignore it and you get a rotated output. createImageBitmap() applies the orientation when it decodes, which is why it is the entry point above instead of an <img>.

2. Transparency turns black. A Canvas starts transparent, and JPEG has no alpha, so transparent pixels encode as black. If you are converting PNG → JPEG, paint a white rectangle first, as above. If you keep PNG, be aware that quality is ignored entirely — PNG is lossless, so the "compressor" just re-encodes it.

3. Re-encoding can make files bigger. This is the one nobody warns you about. Take an already-optimised PNG, run it through a browser encoder at the same dimensions, and you frequently get a larger file. So compare sizes and keep the original when the encoder loses:

const out = await compress(file, settings.quality);
const keptOriginal = out.size >= file.size && /* same dims & format */ true;
return keptOriginal ? file : out;
Enter fullscreen mode Exit fullscreen mode

A compressor that silently returns a bigger file is worse than no compressor, so the UI has to say "already optimised" instead of showing a fake −12%.

4. Canvas size limits. Browsers cap canvas dimensions, and past the limit you often get a silently blank result rather than an error. Around 16384 px on the longest edge is the safe cross-browser zone, so clamp first:

const MAX = 16384;
const largest = Math.max(width, height);
if (largest > MAX) {
  const scale = MAX / largest;
  width = Math.max(1, Math.round(width * scale));
  height = Math.max(1, Math.round(height * scale));
}
Enter fullscreen mode Exit fullscreen mode

When this is the wrong tool

Honest limits:

  • toBlob('image/webp') support is uneven (Safari added it in 16.4), and AVIF encoding is not available this way at all. If you want the smallest file, sharp/mozjpeg/svt-av1 on a server still wins, because browser encoders are optimised for speed, not density.
  • Huge batches belong on a server or in a worker; doing it on the main thread freezes the UI.
  • HEIC input only decodes where the browser natively supports it — Safari/macOS and Windows with the HEIF extension. Chrome on Linux generally cannot. A site that claims universal HEIC conversion in the browser is shipping a WASM decoder; check the bundle size if it matters to you.

What I shipped

I put this exact pipeline behind a page — quality slider, optional resize, format choice, before/after sizes, drag-and-drop, no account: ToolStack Image Compressor.

It is part of a set of 32 browser-local tools (JSON, Base64, image compression/resizing/cropping, colour converters, unit conversion, calculators, QR codes), with the interface in 7 languages. No sign-up, and nothing is uploaded — for the image tools that is createImageBitmap + Canvas, exactly as above.

Disclosure, since I would check it on someone else's page: GA4 page-level analytics is enabled; image data and text input never leave the tab, and there is no server-side storage of anything you paste or drop.

https://tools.mindutil.com/

Top comments (0)