NNicheStackTutorials
← All tutorials
Frontend Tools

imgshrink: Client-Side JPEG Compression Before Cloudinary Quota Hit

Blog contributors uploaded 8MB phone photos. Canvas-based compressor cut uploads 85% with quality slider and max-width — no server.

·12 min read·Frontend Tools
imgshrink: Client-Side JPEG Compression Before Cloudinary Quota Hit cover

Tested on: Chrome 136, iOS Safari 18 Before: Average post image 4.2MB. After: 380KB at quality 0.82, 1600px max width.

Full compressor

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Image Compressor</title>
  <style>
    body { font-family: system-ui; max-width: 720px; margin: 2rem auto; padding: 0 1rem; }
    label { display: block; margin: .75rem 0; }
    canvas { max-width: 100%; border: 1px solid #e2e8f0; }
    .meta { font-size: 14px; color: #475569; }
  </style>
</head>
<body>
  <h1>Image Compressor</h1>
  <input type="file" id="file" accept="image/*">
  <label>Max width (px): <input type="number" id="maxW" value="1600" min="100" max="4000"></label>
  <label>JPEG quality: <input type="range" id="quality" min="0.1" max="1" step="0.01" value="0.82"> <span id="qval">0.82</span></label>
  <button id="dl" disabled>Download JPEG</button>
  <p class="meta" id="meta"></p>
  <canvas id="c" hidden></canvas>
  <script>
    const fileInput = document.getElementById('file');
    const maxW = document.getElementById('maxW');
    const quality = document.getElementById('quality');
    const qval = document.getElementById('qval');
    const canvas = document.getElementById('c');
    const ctx = canvas.getContext('2d');
    const meta = document.getElementById('meta');
    const dl = document.getElementById('dl');
    let blobUrl = null;

    quality.oninput = () => { qval.textContent = quality.value; if (fileInput.files[0]) process(fileInput.files[0]); };
    maxW.onchange = () => { if (fileInput.files[0]) process(fileInput.files[0]); };

    fileInput.onchange = () => {
      const f = fileInput.files[0];
      if (f) process(f);
    };

    function process(file) {
      const img = new Image();
      img.onload = () => {
        const max = Number(maxW.value) || 1600;
        let w = img.width, h = img.height;
        if (w > max) { h = Math.round(h * max / w); w = max; }
        canvas.width = w;
        canvas.height = h;
        ctx.drawImage(img, 0, 0, w, h);
        canvas.toBlob((blob) => {
          if (blobUrl) URL.revokeObjectURL(blobUrl);
          blobUrl = URL.createObjectURL(blob);
          dl.disabled = false;
          dl.onclick = () => {
            const a = document.createElement('a');
            a.href = blobUrl;
            a.download = file.name.replace(/\.[^.]+$/, '') + '-compressed.jpg';
            a.click();
          };
          meta.textContent = 'Original: ' + (file.size/1024).toFixed(0) + ' KB → Compressed: ' + (blob.size/1024).toFixed(0) + ' KB (' + w + '×' + h + ')';
        }, 'image/jpeg', Number(quality.value));
      };
      img.onerror = () => meta.textContent = 'Failed to load image (HEIC may need conversion on desktop).';
      img.src = URL.createObjectURL(file);
    }
  </script>
</body>
</html>

HEIC failure on Windows

Failed to load image

iPhone HEIC isn't decoded everywhere. Document that contributors should enable "Most Compatible" or use Squoosh for HEIC.

EXIF orientation

Canvas ignores EXIF rotation. For production, add a small orientation fix or tell users to rotate before upload.

Memory on 12MP images

Resize before drawImage — never decode full 4000px if output is 1600px. Browser still decodes full res once; acceptable for a tool page.

Verify

Upload 6MB PNG → meta shows ~400KB JPEG Drag quality to 0.5 → file shrinks further Download works offline

Tags: ImagesCanvasCompressionVanilla JS