Skip to content

Loading images responsibly

I've recently come across the terms "responsible" and "responsibly" in the context of web development while reading Responsible Web Applications and Hiding Content Responsibly, which are both great articles. I completely agree that we as developers must be responsible when building applications and do our best to make sure they're inclusive, accessible, performant, responsive, etc.

Today I want to share some techniques I like for handling images in the front end with that mentality.

Before we start, however, here's a quick disclaimer: I use PostCSS with Autoprefixer in my projects for generating CSS that is supported by as many browsers as possible. Because of that, I don't usually write properties with vendor prefixes since they'll be included automatically, but the necessary prefixes for each property in the examples can be found on Can I use.

When using background images, if the file is too large, the user's Internet connection is relatively slow, or the asset download just fails, the background will be blank for at least a few moments. In order to avoid that, it's usually a good idea to have fallbacks in place.

Background colors can serve as simple fallbacks.

.withBackgroundImage {
/* We might want to choose the main color from the background image */
background-color: skyblue;
background-image: url('sky.png');
}

If the background image contains multiple colors, perhaps using a gradient would be more interesting.

.withBackgroundImage {
/* Background image with gradient fallback */
background-image: url('sky.png'), linear-gradient(indigo, skyblue);
}

There are, however, browsers that don't support CSS gradients. To be on the safe side, we can combine the previous examples:

.withBackgroundImage {
/**
* Fallback for when the image can't be loaded and the browser can't handle
* gradients
*/
background-color: skyblue;
/* Fallback for browsers that can't handle gradients */
background-image: url('sky.png');
/* Background image with gradient fallback */
background-image: url('sky.png'), linear-gradient(indigo, skyblue);
}

Here's the introduction from WebP's official page:

WebP is a modern image format that provides superior lossless and lossy compression for images on the web. Using WebP, webmasters and web developers can create smaller, richer images that make the web faster.

In other words, WebP allows us to reduce loading times and save bandwidth while providing images with the same quality. We can also decide to sacrifice some of the quality to make the assets even smaller, so it basically depends on our needs.

There are command-line programs for converting non-WebP images to WebP, e.g., img2webp and gif2webp, as well as sites that offer that service online.

Since not all browsers support WebP, we can use the <picture> element together with the <source> element to only load the WebP image if it's supported. If it's not, the image specified in the <img> element will be loaded instead.

<picture>
<source srcset="decorative-image.webp" type="image/webp" />
<!-- Please remember to use `alt=""` for purely decorative images -->
<img alt="" src="decorative-image.png" />
</picture>

The Wikipedia's "Retina display" page describes it as:

[...] a brand name used by Apple for its series of [...] displays that have a higher pixel density than traditional Apple displays.

Because of the higher pixel density in Retina displays, we may want to use larger images and resize them to their intended size, so that users with Retina displays can get the best image quality possible. However, if they don't own one, we're making them waste bandwidth on data they can't benefit from.

With media queries, we can identify users with Retina displays and serve them the larger image. Since Autoprefixer only handles CSS files, I've generated the vendor prefixes for the HTML file manually using Autoprefixer CSS online.

<!-- index.html -->
<picture>
<source
media="only screen and (-webkit-min-device-pixel-ratio: 2),
only screen and (-o-min-device-pixel-ratio: 2/1),
only screen and (min-resolution: 2dppx)"
srcset="decorative-image-retina.png"
type="image/png"
/>
<!-- Please remember to use `alt=""` for purely decorative images -->
<img alt="" src="decorative-image-normal.png" />
</picture>
/* styles.css */
picture > img {
/* Styles for normal displays */
@media only screen and (min-resolution: 2dppx) {
/* Styles for Retina displays, like resizing */
}
}

We can also combine that technique with WebP media queries:

<!-- index.html -->
<picture>
<source
media="only screen and (-webkit-min-device-pixel-ratio: 2),
only screen and (-o-min-device-pixel-ratio: 2/1),
only screen and (min-resolution: 2dppx)"
srcset="decorative-image-retina.webp"
type="image/webp"
/>
<source
media="only screen and (-webkit-min-device-pixel-ratio: 2),
only screen and (-o-min-device-pixel-ratio: 2/1),
only screen and (min-resolution: 2dppx)"
srcset="decorative-image-retina.png"
type="image/png"
/>
<source srcset="decorative-image-normal.webp" type="image/webp" />
<!-- Please remember to use `alt=""` for purely decorative images -->
<img alt="" src="decorative-image-normal.png" />
</picture>

According to MDN:

The prefers-reduced-motion CSS media feature is used to detect if the user has requested that the system minimize the amount of non-essential motion it uses. [...]

no-preference: Indicates that the user has made no preference known to the system.

reduce: Indicates that user has notified the system that they prefer an interface that removes or replaces the types of motion-based animation that trigger discomfort for those with vestibular motion disorders.

Let's pretend, for example, that we ideally want to use an animated decorative image. If we also generate a static version, this is how we could load them based on reduced motion preferences:

<!-- index.html -->
<picture>
<source
media="(prefers-reduced-motion: no-preference)"
srcset="decorative-image-animated.gif"
type="image/gif"
/>
<!-- Please remember to use `alt=""` for purely decorative images -->
<img alt="" src="decorative-image-static.png" />
</picture>
/* styles.css */
picture > img {
/* Styles for the static image */
@media (prefers-reduced-motion: no-preference) {
/* Styles for the animated image */
}
}

The cool thing about checking for no-preference instead of reduce is that, if the browser doesn't support prefers-reduced-motion, the fallback image is the static one, so we don't risk causing discomfort for users that have vestibular motion disorders.

To test whether our code works, we can follow MDN's reduced motion preferences instructions to start sending reduce in our requests and make sure the application correctly loads a static image.

And, adding some WebP media queries, we end up with:

<!-- index.html -->
<picture>
<source
media="(prefers-reduced-motion: no-preference)"
srcset="decorative-image-animated.webp"
type="image/webp"
/>
<source
media="(prefers-reduced-motion: no-preference)"
srcset="decorative-image-animated.gif"
type="image/gif"
/>
<source srcset="decorative-image-static.webp" type="image/webp" />
<!-- Please remember to use `alt=""` for purely decorative images -->
<img alt="" src="decorative-image-static.png" />
</picture>

Quoting the introduction from MDN's article on lazy loading:

Lazy loading is a strategy to identify resources as non-blocking (non-critical) and load these only when needed. It's a way to shorten the length of the critical rendering path, which translates into reduced page load times.

Merriam-Webster defines "below-the-fold" as "located below the fold on the front page of a broadsheet newspaper." In the context of web applications, it refers to the portion of the page that is not visible without vertically scrolling to it.

Lazy loading images is quite simple; all it takes is setting the loading attribute to "lazy". Here's an example:

<!-- Please remember to use `alt=""` for purely decorative images -->
<img alt="" loading="lazy" src="decorative-image.png" />

When the browser sees that, it will only load the image after the critical content has been loaded and, depending on how far down the page it is, only when the user scrolls near it.