MiniPx
Blog
๐Ÿ”’ Images never leave your browser
โ† Blog

Client-Side Image Compression with JavaScript

By Gaurav Bhowmickยทยท8 min read

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

How does browser-based image compression work?
The browser draws your image onto an HTML Canvas element, then exports it as a new file using canvas.toBlob() or canvas.toDataURL(). During export, you specify a quality parameter (0 to 1) that controls how much compression is applied. The browser's built-in encoder handles the actual compression.
What quality setting should I use for JPEG compression?
For most photos, a quality of 0.7 to 0.8 gives a good balance between file size and visual quality. At 0.8, most people cannot tell the difference from the original. Below 0.5, compression artifacts become noticeable. For thumbnails or previews, 0.5 to 0.6 is often acceptable.
Can I compress PNG files with the Canvas API?
You can export a canvas as PNG using toBlob(callback, "image/png"), but PNG does not support a quality parameter in most browsers. The output will be lossless. To reduce PNG file size, you need to reduce the image dimensions or convert it to JPEG/WebP for lossy compression.
Does client-side compression strip EXIF data?
Yes. When an image is drawn onto a Canvas and re-exported, all EXIF metadata (GPS location, camera info, timestamps) is stripped. The Canvas API only captures pixel data, not metadata. This is actually a privacy benefit of client-side compression.
What are the limits of browser-based compression?
Very large images (above 16,384 x 16,384 pixels on most browsers) may hit canvas size limits. Memory usage can spike with large files. The compression algorithms available are limited to what the browser ships โ€” you cannot use MozJPEG or custom codecs without WebAssembly.

Related tools

Compress JPEGCompress PNGConvert JPG to WebP

More from the blog

JPEG vs PNG vs WebP โ€” Best Format for Websites โ†’Optimize Images for Core Web Vitals โ€” Complete Guide โ†’How Browsers Compress Images (Canvas API Explained) โ†’
๐Ÿ”ง
Try MiniPx โ€” free, no signup

Compress, convert, and resize images in your browser. Nothing gets uploaded.

Open MiniPx โ†’