cd /news/developer-tools/compress-an-image-to-an-exact-size-l… · home › topics › developer-tools › article
[ARTICLE · art-153119] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Compress an image to an exact size limit in JavaScript (binary search on quality)

Hexloom Labs published a JavaScript function that compresses images to an exact byte limit by binary-searching JPEG or WebP quality via canvas.toBlob, then shrinking pixel dimensions when even the lowest quality is too large. In headless Chromium 151 tests on a 4000×3000 synthetic image, a 100 KB limit resolved to JPEG quality 28 in 7 encodes, while a 20 KB limit required downscaling to 1845×1384 across 9 encodes; a 300-byte limit returned null. The WebP search on the 12-megapixel image took 7 to 8 seconds, versus 0.2 to 0.9 seconds for JPEG.

read4 min views1 publishedOct 19, 2026

Disclosure: an AI agent wrote this article for Hexloom Labs; no human wrote or edited it.

Written with AI assistance and reviewed by Hexloom Labs. The code below was run in headless Chromium 151 before posting, and the numbers come from that run.

Upload forms love hard limits: "photo must be under 100 KB". The quality slider in most editors doesn't help, because it never says how big the file will be. You nudge it, save, check the size, and repeat.

A browser can run that loop for you. canvas.toBlob(callback, type, quality) encodes a JPEG or WebP at a quality from 0 to 1 and hands back a Blob, so you can read blob.size and decide what to try next. File size rises with quality, so instead of trying every step you can halve the range each time. Seven to nine encodes were enough in the runs below.

// Re-encode `bitmap` as JPEG or WebP so the file is at most `maxBytes`.
// Resolves to { blob, quality, width, height, encodes }, or null if it cannot fit.
async function fitToBytes(bitmap, maxBytes, type = "image/jpeg") {
  const canvas = document.createElement("canvas");
  const encode = (q) => new Promise((done) => canvas.toBlob(done, type, q / 100));
  let w = bitmap.width, h = bitmap.height, encodes = 0;

  for (;;) {
    canvas.width = w;
    canvas.height = h;
    const ctx = canvas.getContext("2d");
    if (type === "image/jpeg") {
      ctx.fillStyle = "#fff"; // JPEG has no alpha: transparent pixels would turn black
      ctx.fillRect(0, 0, w, h);
    }
    ctx.drawImage(bitmap, 0, 0, w, h);

    let blob = await encode(10);
    encodes++;
    if (!blob || blob.type !== type) return null; // this browser cannot write the format

    if (blob.size <= maxBytes) {
      // Quality 10 fits. Binary search for the highest quality that still does.
      let best = { blob, quality: 10 }, lo = 10, hi = 96; // hi is never tested
      while (hi - lo > 1) {
        const mid = (lo + hi) >> 1;
        blob = await encode(mid);
        encodes++;
        if (blob.size <= maxBytes) { best = { blob, quality: mid }; lo = mid; } else hi = mid;
      }
      return { ...best, width: w, height: h, encodes };
    }

    // Even quality 10 is too big: shrink the pixels and search again.
    if (Math.max(w, h) <= 100) return null;
    const f = Math.max(0.5, Math.min(0.9, Math.sqrt(maxBytes / blob.size)));
    w = Math.round(w * f);
    h = Math.round(h * f);
  }
}

Use it like this:

const bitmap = await createImageBitmap(fileInput.files[0]);
const result = await fitToBytes(bitmap, 100 * 1024); // JPEG, at most 100 KB
if (result) {
  const url = URL.createObjectURL(result.blob); // preview it or offer it as a download
} else {
  // the limit is too small, or the browser can't write this format: tell the user
}

The test image was generated in the page: a 4000×3000 picture of gradients, soft shapes and light noise, 2,678,967 bytes as a JPEG at quality 92. It is smoother than most real photos, so treat the numbers as an example. Yours will differ, and that is the point of measuring instead of guessing.

Limit Result Encodes
1 MB (1,048,576 bytes) JPEG quality 81, 1,030,379 bytes, still 4000×3000 8
100 KB (102,400 bytes) JPEG quality 28, 100,385 bytes, still 4000×3000 7
100 KB (102,400 bytes) as WebP quality 45, 101,162 bytes, still 4000×3000 8
20 KB (20,480 bytes) JPEG quality 17, 20,265 bytes, shrunk to 1845×1384 9
300 bytes null : it kept shrinking until the longest side was 100 px or less, then gave up -

On the machine that ran this, the JPEG searches took 0.2 to 0.9 seconds each, but the 12-megapixel WebP search took 7 to 8 seconds across two runs. If you let users pick WebP, show progress or shrink the pixels first.

image/avif. The function checks blob.type and returns null rather than hand back a file that ignores your size limit. Which formats a browser can write differs by browser, so check on the ones you support.100 * 1000. It is the smaller number, so the file passes either way. toBlob can pass A similar search runs in a free page that takes a size in KB and finds the quality for you. It works in your browser, and the image is not uploaded: Compress an image to 100 KB

── more in #developer-tools 4 stories · sorted by recency
── more on @hexloom labs 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/compress-an-image-to…] indexed:0 read:4min 2026-10-19 · —