Will's avatar

⬅️ See more posts

How to resize images client-side in your webapps

6 July 2021 (4 minute read)

🔮 This post is also available via Gemini.

100daystooffload technology

💯 100 Days to Offload

This article is one of a series of posts I have written for the 100 Days to Offload challenge. Disclaimer: The challenge focuses on writing frequency rather than quality, and so posts may not always be fully planned out!

View other posts in this series.

The problem

Image processing and resizing is a common task in many types of applications. This is made even more important by modern phones that take photos several megabytes in size.

For example, if you offer an application that allows people to choose an avatar image, you won’t want to render the full multi-MB size image each time it’s shown. This is extra data for your users to download each time (which costs them both time and money, and can give a poor sluggish experience) and also means you need to fork out more cash for the additional bandwidth usage. If you have lots of users, then this time/money saving can be amplified significantly.

Lots of tools exist for server-side processing. However this may mean you need to invest in infrastructure to handle asynchronous or scheduled server-side processing, or additional compute capacity to process image uploads on-the-fly.

I’m a big fan of letting the users’ clients do more of the work, since this helps to distribute the compute power required. Allowing your user clients to upload files directly to a service such as S3 is often quicker and means you don’t need to provision and pay for the bigger server capacity yourself to deal with centralised processing of image files.

So, to return to the avatar example above, what if your web front-end can instead resize the image before it is uploaded? This means you don’t need to do the additional server-side processing, you store less data, and the process is a little more predictable.

Client-side JavaScript resizing

Modern browsers - both on desktops and mobile devices - are more than capable of doing a bit of additional work. For image-resizing, as you might have guessed, this involves making use of the HTML canvas element.

Essentially, the process involves the following steps:

  1. Read in the user-chosen file (e.g. from a file chooser) as a data URL.
  2. Load the image from the file.
  3. Determine the correct dimensions for the image.
  4. Draw the image to the canvas.
  5. Build a new file based on the canvas.

The code below should help illustrate this further.

// Allow the caller to specify the file and a max width/height for the file
const resizeImage = (file, maxWidth, maxHeight) => {
  return new Promise((resolve, reject) => {
    // Create a new FileReader
    const reader = new FileReader();

    // Once the FileReader is ready...
    reader.onload = e => {
      // Create a new image
      const img = new Image();

      // Once the image is ready...
      img.onload = () => {
        // Create a new canvas
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');

        // Determine the new image dimensions based on maxWidth and maxHeight
        let width = img.width;
        let height = img.height;
        if (width > height) {
          if (width > maxWidth) {
            height *= maxWidth / width;
            width = maxWidth;
          }
        } else {
          if (height > maxHeight) {
            width *= maxHeight / height;
            height = maxHeight;
          }
        }
        canvas.width = width;
        canvas.height = height;

        // Draw the image to the canvas with the new sizes
        ctx.drawImage(img, 0, 0, width, height);

        // Build and return the resized image as an image file
        canvas.toBlob(blob => {
          resolve(new File([blob], file.name));
        }, 'image/jpeg', 1);
      }

      // Begin to load the image
      img.src = e.target.result;
    }

    // Begin to load the file to the FileReader
    reader.readAsDataURL(file);
  });
}

// Elsewhere in your code (e.g. after the user has selected an image file)
const resizedFile = await resizeImage(file, 400, 400);

// Now you can upload the resized image, etc....

At this point you can upload the resizedFile directly to a storage provider, such as Amazon S3. In case it helps, there is more information on the direct-to-S3 file upload process in this article.

If you care less about storage cost and would prefer to store multiple image sizes anyway (in case you want to offer users the chance to view the full-sized avatar too), then you can upload both file and resizedFile. You can then choose to serve one or the other in different scenarios.

Either way, the key thing is that you don’t need any extra image-processing on the server. I’m sure there are more efficient methods than this in practice, but if you’re looking for a simple, no-dependency approach then I hope this might help!

✉️ You can reply to this post via email.

📲 Subscribe to updates

If you would like to read more posts like this, then you can subscribe via RSS.