Skip to main content
Image Tools

Client-Side Image Compression: What Happens in the Browser

The real pipeline behind Canvas, OffscreenCanvas and WebCodecs, which formats a browser will and will not encode, and when you still need a server.

By 9 min read
Article title card. A large grey square, an arrow, and a much smaller amber square, with the line: same picture, fewer bytes

Drop a photo into a browser-based compressor and, in the common case, nothing leaves your machine. The page reads the file, decodes it, draws it, encodes it again, and hands back a Blob. That is the entire trick.

The structural part is worth more than any privacy policy. For a scan of a passport or a product shot still under embargo, "the bytes never left this laptop" is something you can check in DevTools in about ten seconds. "We delete uploads after 24 hours" is something you can only believe.

What follows is what the browser is actually doing, which formats it will refuse to produce, and the point at which you should stop and put a server in the loop.

Decode, draw, re-encode

Canvas or WebCodecs, the shape is the same:

  1. Read the file. The File API gives JavaScript a byte stream with no network involved.
  2. Decode. The compressed image becomes raw pixels, RGBA, four bytes each.
  3. Resize, if you are going to. This is the only stage where it can happen.
  4. Re-encode. Write the pixels back out at a target quality.
  5. Hand back a Blob. The page can offer it for download or show it in a preview.

Step two is the one that hurts. A 5472 by 3648 photo out of a mid-range camera is around 20 megapixels, so decoded it occupies roughly 80 MB of memory, no matter that the JPEG on disk was 5 MB. The compression ratio you were admiring exists only on disk.

A 20-megapixel photo moving through four stages: 5 MB as a JPEG file on disk, 80 MB once decoded to RGBA pixels, drawn to a canvas, then encoded back out to a Blob
Peak memory sits in the middle of the pipeline, not at either end.

Canvas, the workhorse

The canvas path works in every browser released in the last decade and is about eight lines:

async function compressJpeg(file, quality = 0.8) {
  const bitmap = await createImageBitmap(file);
  const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
  canvas.getContext('2d').drawImage(bitmap, 0, 0);
  const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality });
  bitmap.close();
  return blob;
}

quality runs from 0 to 1 and drives the encoder's quantization tables for JPEG and WebP. For anything lossless it is simply ignored1, which is the first thing people get wrong about PNG.

OffscreenCanvas lets all of this run in a Web Worker, so a 40 MB image does not freeze the interface. Safari was the last engine to ship it, in 16.42, and 2D is the part that landed.

The formats it will not encode, and how quietly it refuses

Here is the behaviour that costs people an afternoon.

If you pass a MIME type the browser cannot encode, toBlob does not throw and does not return null. The specified fallback is to encode PNG instead1. You get a Blob, your success handler runs, your download works, and the file is a PNG wearing the name you gave it.

A table of five MIME types requested from canvas toBlob and the Blob type returned in each of Chromium 147 and Firefox 148: png, jpeg and webp return themselves, avif and jxl both return image/png
Two engines, same answer. AVIF and JPEG XL are not canvas output formats in either.

I measured that in headless Chromium 147 and Firefox 148 on 19 August 2026, driving both through Playwright 1.59.1. Two lines will reproduce it in whatever you are running right now:

const blob = await new Promise((r) => canvas.toBlob(r, 'image/avif', 0.8));
console.log(blob.type); // "image/png" in Chromium 147 and Firefox 148

For a photograph this is worse than an error, because a PNG of a decoded photo is routinely larger than the JPEG you fed in. The compressor reports success and the file grows.

FormatCanvas can encode itNotes
PNGYesLossless, so quality does nothing
JPEGYesNative codec, hardware-accelerated on most devices
WebPYes25 to 34 percent smaller than JPEG at the same SSIM4
AVIFNo, returns PNGNeeds WebCodecs plus your own muxing, or a WASM encoder
JPEG XLNo, returns PNGSame

So the pragmatic default for a browser compressor in 2026 is WebP at quality: 0.85. It is the best format the platform will actually hand you.

WebCodecs has no image encoder

This is the part most write-ups get backwards, including the earlier version of this article. WebCodecs is not a faster route to AVIF stills. It is a video API.

ImageDecoder exists, and it is genuinely useful for pulling frames out of an animated image. There is no ImageEncoder. I checked for one in both engines above and it is undefined in each.

What you can do is encode a single AV1 keyframe with VideoEncoder, which both Chromium and Firefox report as supported for av01.0.04M.08, and then assemble an AVIF container around the chunk yourself. That is a real technique, and it is also a container-format project rather than a compression setting. VideoEncoder is restricted to secure contexts3, so it will be undefined on a page served over plain HTTP, which is a confusing way to discover the requirement. Safari has had the video half of WebCodecs since 16.42.

If you want AVIF in a browser without writing a muxer, ship a WebAssembly encoder. Squoosh took exactly this route, bundling codec builds to get encoders the platform does not expose6. It works. The bundles are megabytes, and your users pay that on first load.

PNG is a different problem entirely

There is no quality knob because nothing is being thrown away. Size comes from the deflate level and, far more, from the content: flat colour compresses beautifully, photographic grain barely compresses at all.

Three things actually shrink a PNG.

Reduce the colour depth. An image using 256 colours belongs in an 8-bit palette, not 24-bit truecolour. Doing this in a browser means a WebAssembly build of something like pngquant, because canvas will not do it for you.

Change format. A photograph that arrived as a PNG is in the wrong container, and WebP at 0.85 will be dramatically smaller at similar visual quality. Our Image Format Converter covers that case.

Resize. Blunt, and more effective than either of the above.

Memory, at the point where it breaks

One image at a time is fine on any machine made this decade. Batches are where tabs die.

Thirty holiday photos at 20 megapixels each is 2.4 GB of decoded pixels if you kick off all thirty decodes at once, and mobile Safari will kill the tab well before that. The fix is unglamorous: process serially, and call close() on each ImageBitmap before starting the next, which disposes of the graphical resources behind it5. Skipping that call is the single most common cause of out-of-memory crashes in browser-side image tools.

Two in flight is a reasonable ceiling if you want to keep more than one core busy.

Where a server still wins

Production batch pipelines. Running a marketing team's asset pipeline through a browser tab works right up until someone closes the tab. libvips on a server is faster, more configurable, and does not depend on a human being present.

AVIF and JPEG XL at any volume. Covered above: the platform will not encode either, and the WASM route costs both bundle size and encode time.

Encoder-specific tuning. Chroma subsampling, mozjpeg quantization tables, palette quantisation. All of it needs a real encoder binary.

Colour-managed work. A canvas has its own colour space, so a wide-gamut ICC profile embedded by the camera does not survive the round trip. For print or medical imaging that shift is not acceptable, and the compression belongs somewhere that preserves the profile.

Checking the privacy claim rather than believing it

When compression happens on a canvas, the pixel data has no route to a server. That is a property of the code path, not a promise, and it holds for anything sensitive you might drop into one of these tools: a photographed document, a screenshot of a private thread, mockups under NDA.

You can verify it in under a minute. Open DevTools, go to the Network tab, compress an image, and watch for a request carrying the file. With an upload-based tool you will see it immediately. With a canvas-based one there is nothing to see. That test generalises well past image tools, which is most of what the online privacy guide is about.

Which API to reach for

  • Resize and output JPEG or WebP: createImageBitmap plus OffscreenCanvas.convertToBlob, in a worker. Fast, universal, boring in the good way.
  • AVIF or JPEG XL: a WASM encoder, or a server. Not canvas.
  • Palette-reducing a PNG: a WASM build of pngquant. Canvas cannot.
  • Keeping EXIF or ICC data: read it out with a library such as piexifjs before encoding and re-inject afterwards, because canvas keeps none of it.

To see the quality tradeoff without writing any of this, our Image Compressor shows before and after sizes as you move the quality slider, and the Image Resizer handles the dimension half. Both run entirely in the tab.

The browser is a capable image runtime with two sharp edges: it stays quiet about formats it cannot encode, and it holds decoded pixels uncompressed. Design around those two and the rest is straightforward.

Sources

Every number in this article traces to a source below. Where a claim could not be sourced, it was cut rather than softened.

  1. Primary sourceMDN Web Docs

    That an unsupported MIME type falls back to image/png rather than failing, and that the quality argument is ignored for formats without lossy compression.

  2. Primary sourceApple

    That Safari 16.4 added OffscreenCanvas for 2D operations and the video portion of the WebCodecs API.

  3. Primary sourceMDN Web Docs

    That VideoEncoder is available only in secure contexts and is exposed to dedicated workers.

  4. Primary sourceGoogle

    The finding that WebP files are 25 to 34 percent smaller than JPEG at the same SSIM index, measured across JPEG quality levels 50, 75 and 95.

  5. Primary sourceMDN Web Docs

    That close() disposes of all graphical resources associated with an ImageBitmap, which is what releases the decoded pixels.

  6. Primary sourceGitHub

    That a browser-based compressor can ship WebAssembly builds of image codecs to get encoders the platform does not provide.

Topics

  • Image Compression
  • Privacy
  • Browser Apis
  • Performance

Tools mentioned in this article

Get new tools by email

New tools and the occasional deep-dive, about once a month. No spam, no sharing your address, unsubscribe in one click.

Related articles

Article title card. A shield with its right half filled in amber, with the line: what leaves the machine, and when
Security & Privacy Guide

Online Privacy Guide: Threat Models for Browser-Based Work

Threat models worth naming, the thirty-second check that beats any privacy policy, and an honest list of which of our own tools upload your file.

Article title card. A clipboard holding three lines of text with an amber exclamation mark on its corner, with the line: paste is a transfer, not a view
Security & Privacy

Never Paste This Into a Random Online Tool

A working threat model for web tools at work: where a paste actually lands, which data is fine, and the thirty-second check that beats reading a privacy policy.

Article title card. A browser window holding an amber document with a padlock on it, with the line: the file never leaves the tab
PDF Tools

Why Your PDF Tool Should Run in the Browser, Not the Cloud

What an upload actually exposes, which PDF operations a browser can genuinely perform, and which ones cannot run locally at all. Including ours.