Jay Gould

Image performance in components with Cloudinary

November 21, 2021

For those who haven’t used the revolutionary media platform, Cloudinary is a web service which acts as a storage space, allowing users to request their media in a bunch of great ways. For example, a user can request that their image come back with a watermark, or be a rounded shape, or have a sepia filter. One of my favourite, and possibly most useful modifier when receiving an image is to specify the size of an image.

This post briefly covers what Cloudinary is, then shows a way to create reusable image components to make it even easier to pull back images for everyday usage within your website/app.

Retrieving images from Cloudinary

I won’t go in to too much detail here as the full usage can be found on Cloudinary’s website easily enough, but in a nutshell, images are requested and modified using the image URL. For example, here’s the URL of an image on my Cloudinary:

https://res.cloudinary.com/jaygould/image/upload/v1637419964/blogs/beer.jpg

Beer image default

The image above is the default image I uploaded to Cloudinary (taken from a Head of Steam bar in Cardiff, if anyone’s interested). It is a 1200 x 900px jpg on Cloudinary, but using the URL of the image, we’re able to retrieve the image back in any size we want. If we want to bring back a much smaller image for a large thumbnail for example, we can use the following URL:

https://res.cloudinary.com/jaygould/image/upload/c_scale,w_300/v1637419964/blogs/beer.jpg

Beer image small

The URL segment after /upload/ is used to modify the image. In the case above, we’re pulling the image back resized to 300px width using the /c_scale,w_300/ segment. The height of the image is 225px because it will send back the image at its natural aspect ratio.

Similarly, an image can be resized to fit a specific sized area constrained by both width and height:

https://res.cloudinary.com/jaygould/image/upload/c_fill,w_300,h_300/v1637419964/blogs/beer.jpg

Beer image square

The c_fill part of the modifier ensures the image isn’t stretched to fit the new aspect ratio of the image. This means part of the image will be cropped, but it will always fill the space within the defined width and height (in our case, a 300px square).

How is this useful?

The benefit of this approach is that we’re able to pull down the exact size of the image we want without manually resizing, and help save unneeded data transferred. With performance being at the top of everyone’s mind when creating sites and apps, this method of retrieving media from a remote storage repository is invaluable for a developer, saving a huge amount of time and effort which can usually go in to resizing, hosting, updating, etc.

Making JS function to output the image URL

One simple way to abstract the Cloudinary URL structure is to create a simple function to take the everyday parameters you may be using. I like to create a function which takes in the image size, cropping method, and image quality - all parameters to pass into Cloudinary’s URL based modifiers:

// urlFormatter.js

export default (url, { w, h, q, c }) => {
  const width = w ? "w_" + w : ""
  const height = h ? ",h_" + h : ""
  const quality = q ? ",q_" + q : ",q_80"
  const crop = c ? "," + c : ",c_fill"

  const splitDelimiter = "image/upload"
  const split = url.split(splitDelimiter)

  const formattedUrl = `${split[0]}/${splitDelimiter}/${width}${height}${quality}${crop}/${split[1]}`

  return formattedUrl
}

The function above allows us to format a URL for use with Cloudinary’s parameter structure which can be used like this:

// page.js

import React from "react"

import urlFormatter from "../components/urlFormatter"

export default function Home() {
  const imageUrl = urlFormatter(
    "https://res.cloudinary.com/jaygould/image/upload/v1637419964/blogs/beer.jpg",
    { w: 600, h: 400, q: 80, c: "c_fill" }
  )

  return (
    <div>
      <img src={imageUrl} alt="Beer" />
    </div>
  )
}

This may not seem all that useful right now as it takes more code to output an image URL than the original Cloudinary structure, but it’s a base for further enhancements we can re-use later.

Making a high performance image component

Although the Cloudinary image modifying allows us to get back images in any size to help with performance, that’s just one way to get a more performant image back. There are a load of other ways to eek out those last few KB of an image. These include getting the right image for the screen using the “Picture” HTML element, and also lazy loading images using the “loading” HTML attribute.

// PerfImage.js

import React from "react"
// Use the urlFormatted function we created earlier
import urlFormatter from "./urlFormatter"

const PerfImage = ({ src, alt, className, imageParams, mediaSizes }) => {
  if (!src) return null

  // Convert image to webp to get further performance enhancements
  const webpSrc = `${src.split(".").slice(0, -1).join(".")}.webp`

  return (
    <picture>
      {mediaSizes &&
        mediaSizes.length &&
        mediaSizes.map((size) => {
          // Loop over each specified screen media sizes
          return (
            <source
              srcSet={urlFormatter(webpSrc, {
                w: size.width,
                h: size.height,
              })}
              media={size.mediaQuery}
            />
          )
        })}
      <img
        src={urlFormatter(webpSrc, {
          ...imageParams,
        })}
        alt={alt || ""}
        className={`${className ? className : ""}`}
        loading="lazy"
      />
    </picture>
  )
}

module.exports = PerfImage

First, as Cloudinary allows us to retrieve a different image format by specifying in the URL, we’re able to get a huge perf benefit by converting all images to WebP format. Webp is supported in all browsers now, so there’s no reason not to take full advantage of the ”up to 34% smaller” images with little effort.

My beer image in PNG format at original 1200px width is 1.7MB, but when simply updating the URL from beer.png to beer.webp when retrieving from Cloudinary, the file size reduces to 241KB for the same image size. That’s a huge 85.82% reduction in image size.

Then the <picture> element is defined which maps over an array of image sizes we want for each screen size. This allows us to specify a smaller image size for mobile screens for example, which is a great way of serving less data for mobile devices - an important part of performance enhancement when ever last KB makes a huge difference, especially for users who pay for their data by usage.

Below the picture element is the standard <img> tag, which is the default image used as a fallback for browsers which don’t support the picture tag, and also when the screen size does not match one provided as a mediaSizes parameter.

And here’s how we would use the above PerfImage component:

// page.js

import React from "react"

import PerfImage from "../components/PerfImage"

export default function Home() {
  return (
    <div>
      <PerfImage
        src={
          "https://res.cloudinary.com/jaygould/image/upload/v1637419964/blogs/beer.jpg"
        }
        alt={`Performant beer`}
        imageParams={{
          width: 600,
          height: 400,
        }}
        mediaSizes={[
          {
            mediaQuery: "(max-width: 450px)",
            width: 200,
            height: 150,
          },
          {
            mediaQuery: "(max-width: 768px)",
            width: 300,
            height: 270,
          },
        ]}
      />
    </div>
  )
}

Improvements

One addition to this solution could be to allow support for high resolution images. In the current setup we specify the image size which will always show as the rendered size. With high resolution images though, an image is sent back at 500px for example, but is rendered as a lower size to effectively cram more pixels in to the space:

<img src="image-500px.png" width="250" height="250" />

So our 500px image could be restricted to half the size to make the image 2x the pixel density.

Another addition could be added for browsers which don’t support the lazy loading attribute. This is currently not supported by Safari, Opera and a few other browsers, and as Safari is a huge chunk of iOS mobile users, it would be great to have a fallback for this.

Finally, a further addition could be to add a fallback for WebP images. Although WebP is not supported in IE, some sites are still required to run in older browsers, so it’s something to keep in mind.


Senior Engineer at Haven

© Jay Gould 2023, Built with love and tequila.