closeup of medieval helmet

The Complexity of Images in Modern Web Dev, and How We Do it on Foolhat

Making images fast, responsive and accessible in modern web development is hard. Let's talk about it, and how we do things on this site.

Here are some of the ideas we have about handling the complexity and making a great experience for your users!


What features do we need to make a great experience around images on a modern site?

Here's a list of features and desirable qualities that are required to make a great experience for users when it comes to images on a modern site:

Optimising Image Formats & Quality

We've chosen to do things the hard way on this site. We generate a full set of optimised assets for each image on the site, so image optimisation is handled before the build process (using 11ty). We do this by putting images we want to optimise in a directory, and running the script below that calls ImageMagick. Generally the source images are ~3840px on the longest edge prior to transformation.

    for i in *.jpg;
do name=`echo $i | cut -d'.' -f1`;
echo $name;
convert "$i" -strip -interlace Plane -resize 2560 -quality 70% "${name}-2560.webp";
convert "$i" -strip -interlace Plane -resize 1920 -quality 70% "${name}-1920.webp";
convert "$i" -strip -interlace Plane -resize 1560 -quality 70% "${name}-1560.webp";
convert "$i" -strip -interlace Plane -resize 1200 -quality 70% "${name}-1200.webp";
convert "$i" -strip -interlace Plane -resize 900 -quality 70% "${name}-900.webp";
convert "$i" -strip -interlace Plane -resize 600 -quality 70% "${name}-600.webp";
convert "$i" -strip -interlace Plane -resize 450 -quality 70% "${name}-450.webp";
convert "$i" -strip -interlace Plane -resize 360 -quality 70% "${name}-360.webp";
convert "$i" -strip -interlace Plane -resize 300 -quality 70% "${name}-300.webp";
convert "$i" -strip -interlace Plane -resize 200 -quality 70% "${name}-200.webp";
convert "$i" -strip -interlace Plane -resize 2560 -quality 70% "${name}-scale-2560.jpg";
convert "$i" -strip -interlace Plane -resize 1920 -quality 70% "${name}-scale-1920.jpg";
convert "$i" -strip -interlace Plane -resize 1560 -quality 70% "${name}-scale-1560.jpg";
convert "$i" -strip -interlace Plane -resize 1200 -quality 70% "${name}-scale-1200.jpg";
convert "$i" -strip -interlace Plane -resize 900 -quality 70% "${name}-scale-900.jpg";
convert "$i" -strip -interlace Plane -resize 600 -quality 70% "${name}-scale-600.jpg";
convert "$i" -strip -interlace Plane -resize 450 -quality 70% "${name}-scale-450.jpg";
convert "$i" -strip -interlace Plane -resize 360 -quality 70% "${name}-scale-360.jpg";
convert "$i" -strip -interlace Plane -resize 300 -quality 70% "${name}-scale-300.jpg";
convert "$i" -strip -interlace Plane -resize 200 -quality 70% "${name}-scale-200.jpg";
convert "$i" -strip -interlace Plane -resize 30 -gaussian-blur 1 -quality 20% "${name}-blurThumb.jpg";
done

You can adjust the parameters to your taste. For example, if you're a photographer with high standards for the way your photos are presented, you may want to increase the quality value here.


Generating Low Quality Placeholders

You'll see in the last conversion line in the script generates a .jpg file that's 30px wide, with very low quality and a gaussian blur applied. This image is set as the background for the containing div we put around all of our images. We'll go into this in more detail later.

Focal Points - How we Keep Important Details on Screen

One of our bug bears one websites is background images that get awkwardly cropped on different screen sizes. We avoid this by specifying a "focal point" for each of our images, expressed as a "x% y%" on a --focus CSS custom property on each image element. This property gets mapped onto the object-position property for background images on our site.

We prefer to use img tags with object-fit as background images on our site, so we can retain all of the performance and accessibility benefits of our image strategy, despite the comparative simplicity of using CSS background-images. We find that background images like that either don't have high enough quality for larger screens, or they have poor loading performance on mobile devices.

Keeping Images Accessible

There's nothing special or complex here - we just make sure to always include alt text for our images. We generate the markup for our images using an 11ty shortcode, so we make things easy for dynamic content by including an "alt" value in the metadata for each of the pages & posts on this site. That way, we can easily add alt text to featured images in blog post lists, and og:alt & twitter:alt text to improve accessibility of our content when it's shared on social media.


The Markup that Makes it Work

The easiest way to discuss the markup we use is to show our 11ty shortcode, so here it is!

eleventyConfig.addShortcode("image", function (image, alt, focus, width, height) { return `
    <div class="ic" style="--placeholder:url('/img/${image}-blurThumb.jpg')"> 
        <img alt="${alt}" decoding="async" data-src="/img/${image}.jpg" width="${width}" height="${height}"
            data-srcset="/img/${image}-200.webp 200w, /img/${image}-300.webp 300w, /img/${image}-450.webp 450w,
            /img/${image}-600.webp 600w, /img/${image}-900.webp 900w, /img/${image}-1200.webp 1200w, 
            /img/${image}-1560.webp 1560w, /img/${image}-1920.webp 1920w, /img/${image}-2560.webp 2560w" 
            style="--focus: ${focus}"/> 
        <noscript> 
            <img alt="${alt}" src="/img/${image}-scale-1200.jpg" width="${width}" height="${height}" style="--focus: ${focus};opacity: 1;height: 100%;"/> 
        </noscript>
    </div>` }); '

Breaking Down the attributes

Alt

The shortcode accepts an "alt" as the second argument, because it's a high priority. You'll see in the markup that the alt value is passed through to the image element's alt attribute.

Decoding

Setting the decoding attribute to async tells browsers to decode the image asynchronously to prevent potential delays in presenting other content. This is fine here, because we already have a blurred placeholder image in place.

Width & Height

The width & height attributes are set to inform browsers of the intrinsic aspect ratio of images, which helps to prevent CLS during loading. It's not the most important to us on this site, as most of our images are in flexible containers.

Data-src & Data-srcset

Our site has a script that uses an intersection observer to determine when images enter the viewport. Upon entry, the data-src and data-srcset values are set as src and srcset attributes to begin loading the image.

The inline style

We use an inline style to set the --focus custom property with a focus value. This is the practical application of the focal points mentioned previously in this post.


Is this worth the trouble?

A fair question to ask, and for this site it definitely is. The approach we've outlined here is great for performance, but comes with a lot of manual work and mental overhead. For solo devs & small teams working on small projects, it's probably fair to say it's worth the effort. At scale, it's not going to be as easy an argument to make.

That being said, the benefits of improved performance and accessibility are undeniable for businesses of any size, and there's no doubt that lessons could be learned from our approach and applied & automated at a larger scale.

We take a similar approach on WordPress sites, but with less fine-grained control over the markup, and the expectation that clients won't be as meticulous with alt text & focal points, what constitutes a "good solution" is different.