DEV Community

Cover image for Build a Client-Side Image Compressor in 30 Minutes (No Backend Needed)
Vijay Kanna
Vijay Kanna

Posted on

Build a Client-Side Image Compressor in 30 Minutes (No Backend Needed)

Most image compression tutorials assume you're running something server-side — Sharp, ImageMagick, a Node backend. This one doesn't need any of that. We're going to build a working image compressor that runs entirely in the browser, using nothing but the Canvas API.

By the end, you'll have a single HTML file that takes an image, compresses it, and lets the user download the result — no server, no dependencies, no build step.

What We're Building

Final browser-based image compressor application built in this tutorial

A simple page where a user can:

  1. Upload an image
  2. Adjust a quality slider
  3. See the compressed file size update live
  4. Download the compressed result ## Step 1: The HTML Skeleton
<!DOCTYPE html>
<html>
<head>
  <title>Image Compressor</title>
</head>
<body>
  <input type="file" id="fileInput" accept="image/*">
  <br><br>
  <label>Quality: <span id="qualityValue">80</span>%</label>
  <input type="range" id="qualitySlider" min="1" max="100" value="80">
  <br><br>
  <canvas id="canvas" style="max-width: 400px; display: none;"></canvas>
  <p id="sizeInfo"></p>
  <a id="downloadLink" style="display: none;">Download Compressed Image</a>

  <script src="compress.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Nothing fancy — a file input, a quality slider, a canvas to do the actual work, and a download link that appears once we have a result.

Step 2: Loading the Image Into Canvas

Canvas can't compress a file directly — it works with pixels. So the first job is getting the uploaded image drawn onto a canvas:

const fileInput = document.getElementById('fileInput');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let currentImage = null;

fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  if (!file) return;

  const bitmap = await createImageBitmap(file);
  currentImage = bitmap;

  canvas.width = bitmap.width;
  canvas.height = bitmap.height;
  ctx.drawImage(bitmap, 0, 0);
  canvas.style.display = 'block';

  compressAndPreview();
});
Enter fullscreen mode Exit fullscreen mode

createImageBitmap is the modern way to decode an image file — it's faster than the older Image() + onload pattern and works well with async/await.

If you need to support older browsers that don't implement createImageBitmap, you can fall back to using an Image element with an onload handler:

function loadImageFallback(file) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.onerror = reject;
    img.src = URL.createObjectURL(file);
  });
}
Enter fullscreen mode Exit fullscreen mode

Both approaches draw onto the canvas the same way afterward — ctx.drawImage(bitmap, 0, 0) works whether bitmap came from createImageBitmap or this fallback.

Step 3: Compressing With toBlob

This is the actual compression step. canvas.toBlob() takes a format and a quality value (0 to 1) and re-encodes the canvas content:

const qualitySlider = document.getElementById('qualitySlider');
const qualityValue = document.getElementById('qualityValue');
const sizeInfo = document.getElementById('sizeInfo');
const downloadLink = document.getElementById('downloadLink');

function compressAndPreview() {
  if (!currentImage) return;

  const quality = qualitySlider.value / 100;

  canvas.toBlob((blob) => {
    const sizeKB = (blob.size / 1024).toFixed(1);
    sizeInfo.textContent = `Compressed size: ${sizeKB} KB`;

    const url = URL.createObjectURL(blob);
    downloadLink.href = url;
    downloadLink.download = 'compressed.jpg';
    downloadLink.textContent = `Download (${sizeKB} KB)`;
    downloadLink.style.display = 'inline';
  }, 'image/jpeg', quality);
}

qualitySlider.addEventListener('input', () => {
  qualityValue.textContent = qualitySlider.value;
  compressAndPreview();
});
Enter fullscreen mode Exit fullscreen mode

That's the entire core mechanism. Move the slider, toBlob re-encodes at the new quality, and the size updates live.

Step 4: A Small but Important Fix — Memory Cleanup

Every time compressAndPreview() runs, it creates a new object URL with URL.createObjectURL(). If you don't release the old one, you leak memory — this adds up fast if someone drags the slider around for a while:

let previousUrl = null;

function compressAndPreview() {
  if (!currentImage) return;

  const quality = qualitySlider.value / 100;

  canvas.toBlob((blob) => {
    if (previousUrl) URL.revokeObjectURL(previousUrl);

    const sizeKB = (blob.size / 1024).toFixed(1);
    sizeInfo.textContent = `Compressed size: ${sizeKB} KB`;

    const url = URL.createObjectURL(blob);
    previousUrl = url;
    downloadLink.href = url;
    downloadLink.download = 'compressed.jpg';
    downloadLink.textContent = `Download (${sizeKB} KB)`;
    downloadLink.style.display = 'inline';
  }, 'image/jpeg', quality);
}
Enter fullscreen mode Exit fullscreen mode

Small detail, easy to miss, and exactly the kind of thing that turns into a real memory leak if this code ever ships in something long-running.

What This Doesn't Handle (On Purpose)

This tutorial deliberately stops at "quality slider compression" because that's enough to actually understand the mechanism. A few things it leaves out, if you want to take it further:

  • Target file size instead of quality — this needs a search loop around toBlob (binary search on quality, falling back to resizing dimensions when quality bottoms out) rather than a direct slider
  • PNG supporttoBlob works for PNG too, but PNG is lossless, so "quality" doesn't apply the same way; you'd be resizing or reducing color depth instead
  • Large image performance — very large images (many megapixels) can make toBlob noticeably slow; moving this to a Web Worker keeps the UI responsive during compression ## Next Challenges

Once you've built this version, try extending it with:

  • Drag-and-drop uploads
  • Batch compression
  • WebP output
  • AVIF output
  • Exact target-size compression
  • EXIF orientation handling ## Try It Yourself

Paste both snippets into an HTML file and open it in a browser — no build tools, no npm install, nothing to configure. That's the whole appeal of doing this client-side: it's genuinely just HTML, CSS, and the Canvas API doing real work.

If you want to see a fuller version of this same idea — target-size compression, PDF tools, and 70+ other browser-based utilities built on the same no-backend approach — I've been building ResizeHub using exactly this pattern, extended a lot further.

Top comments (0)