FSIBLOG

Resize Image in JavaScript to Avoid a Huge File: Why Canvas Turns a 3 MB Photo Into 22 MB

Resize Image JavaScript to Avoid Huge File: The Canvas Trap

The short answer: draw the photo onto a smaller canvas, then save it with canvas.toBlob(callback, 'image/jpeg', 0.82). If you leave out the type, the browser saves a PNG. For a photo, a PNG can be bigger than the file you started with.

What Happened to Our Test Photo at Each Step

MethodSizeCompared with original
Original photo3.0 MB–
toDataURL(), full size, no type30.2 MB of text10× bigger
toBlob(), full size, no type22.7 MB7.5× bigger
1600 px, PNG4.1 MB35% bigger
1600 px, JPEG 0.92432 KB86% smaller
1600 px, JPEG 0.82286 KB91% smaller
1600 px, JPEG 0.82, as a data URL381 KB33% more than the Blob
1600 px, WebP 0.82202 KB94% smaller
1600 px, JPEG 0.60187 KB94% smaller

Your numbers will differ by photo. The order will not PNG is always the largest for camera photos, and JPEG or WebP is always far smaller.

Five Reasons a Resized Image Ends Up Bigger Than the Original

  1. No type given means PNG. canvas.toBlob(cb) and canvas.toDataURL() both save PNG by default. PNG is lossless. It stores every grain of camera noise, so photos balloon.
  2. Quality is ignored for PNG. toBlob(cb, 'image/png', 0.5) does nothing with the 0.5. The quality number only works for JPEG and WebP.
  3. Data URLs add a third. toDataURL() returns Base64 text, which is about 33% larger than the same image as a Blob. Upload the Blob instead.
  4. Safari cannot save WebP. toBlob(cb, 'image/webp') in Safari on Mac and iPhone quietly returns a PNG. The same happens in any browser for a type it does not support.
  5. The canvas is still full size. If the canvas width and height match the original photo, nothing was resized. Only the format changed.

Add the Resize Function That Keeps Files Under a Size Limit

Save this as resize-image.js. It has no dependencies. It only uses standard browser features that current Chrome, Edge, Firefox and Safari all support. We ran it in Chromium for the numbers in this article.

// Resize an image File in the browser and keep the result under a size limit.
async function resizeImage(file, options = {}) {
  const {
    maxWidth = 1600,
    maxHeight = 1600,
    maxBytes = 500 * 1024,   // 500 KB
    type = 'image/jpeg',
    quality = 0.82,
    minQuality = 0.5,
  } = options;

  // 1. Decode the file. createImageBitmap turns the photo the right way up.
  const bitmap = await createImageBitmap(file);

  // 2. Work out the new size without stretching the picture.
  let scale = Math.min(1, maxWidth / bitmap.width, maxHeight / bitmap.height);
  let q = quality;
  let blob;

  // 3. Draw, encode, and tighten until the file fits.
  for (let attempt = 0; attempt < 8; attempt++) {
    const width = Math.round(bitmap.width * scale);
    const height = Math.round(bitmap.height * scale);
    const canvas = drawScaled(bitmap, width, height, type);

    blob = await new Promise((resolve) => canvas.toBlob(resolve, type, q));

    // Safari cannot make WebP and quietly hands back a PNG instead.
    if (blob.type !== type) {
      blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/jpeg', q));
    }

    if (blob.size <= maxBytes) break;
    if (q > minQuality) q = Math.max(minQuality, q - 0.1);
    else scale *= 0.8;
  }

  bitmap.close();

  // 4. Never upload something bigger than what the user picked.
  return blob.size < file.size ? blob : file;
}

// Shrink in halves first, then do the last step. This keeps edges sharp
// in browsers that ignore imageSmoothingQuality.
function drawScaled(source, width, height, type) {
  let current = source;
  let w = source.width;
  let h = source.height;

  while (w / 2 > width) {
    w = Math.round(w / 2);
    h = Math.round(h / 2);
    const step = document.createElement('canvas');
    step.width = w;
    step.height = h;
    const sctx = step.getContext('2d');
    sctx.imageSmoothingQuality = 'high';
    sctx.drawImage(current, 0, 0, w, h);
    current = step;
  }

  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext('2d');
  ctx.imageSmoothingQuality = 'high';

  // JPEG has no transparency, so paint white first or clear areas turn black.
  if (type === 'image/jpeg') {
    ctx.fillStyle = '#ffffff';
    ctx.fillRect(0, 0, width, height);
  }

  ctx.drawImage(current, 0, 0, width, height);
  return canvas;
}

What Each Part of the Function Does

Resize the Photo Before It Leaves the Browser

This page takes a picked photo, shrinks it, shows the before and after size, and uploads the small version.

<input type="file" id="photo" accept="image/*">
<p id="info"></p>
<img id="preview" alt="Resized preview" style="max-width:100%">

<script src="resize-image.js"></script>
<script>
document.getElementById('photo').addEventListener('change', async (event) => {
  const file = event.target.files[0];
  if (!file) return;

  const small = await resizeImage(file);

  document.getElementById('info').textContent =
    `${(file.size / 1048576).toFixed(2)} MB → ${Math.round(small.size / 1024)} KB`;
  document.getElementById('preview').src = URL.createObjectURL(small);

  const form = new FormData();
  form.append('photo', small, file.name.replace(/\.\w+$/, '.jpg'));
  await fetch('/upload', { method: 'POST', body: form });
});
</script>
Tested result: our 3.0 MB test photo went in. The page showed 3.04 MB → 286 KB, and the server received a 292,669-byte upload.

Choose the Width, Format and Quality for Your Use

Where the image is shownmaxWidthqualitymaxBytes
Profile picture or avatar4000.8060 KB
Product or listing photo12000.82200 KB
Blog or article image16000.82400 KB
Full-screen gallery25600.85900 KB

Example for a product photo:

const small = await resizeImage(file, {
  maxWidth: 1200,
  maxHeight: 1200,
  maxBytes: 200 * 1024,
});

In our test this gave a 1200 × 900 JPEG of 186 KB in about a quarter of a second.

Check the Result in the Browser Console

Before you blame the server, check what the browser actually made. Paste this into the console after a Library:

const bmp = await createImageBitmap(small);
console.table({
  type: small.type,
  sizeKB: Math.round(small.size / 1024),
  width: bmp.width,
  height: bmp.height,
});

Mistakes That Bring the Huge File Back

Exit mobile version