Effective Images For The Web

#Article

This is a guide to using the HTML <img> tag and optimizing for bandwidth and performance. Also referred to as "responsive images".

The <img> tag is arguably one of the most important HTML tags out there. It is one of the things that haven’t been replaced by JavaScript over the years, contrasting <textarea/> or various input types. This is why its API has evolved over the years, enabling browsers to more effectively fetch and render images dynamically.

But what if you just got started with the web and wanted to embed some images into your HTML? The corresponding MDN docs for <img> are great at describing the tag, but fail to provide real world examples as for how to use them effectively. This is what this article is about.

Starting off,

The standard <img> attributes everyone knows about are:

<img
    src="/test-img.webp"
    alt="This is the image description."
    width="250"
    height="200"
>
This is the image description.

This displays an image hosted at the src-URL with a width and height. But what does happen when we omit the image dimensions or just straight up provide false ones?

Omitting width and height

<img
    src="/test-img.webp"
    alt="This is the image description."
>
This is the image description.

Providing false dimensions

<img
    src="/test-img.webp"
    alt="This is the image description."
    width="100"
    height="10000"
>
This is the image description.
<img
    src="/test-img.webp"
    alt="This is the image description."
    width="10000"
    height="100"
>
This is the image description.

As you can see, the image is still rendered, specifically with the actual aspect ratio of the image. Omitting the dimensions causes the image to be rendered at its pixel data size. Setting the width to 10,000 causes the image to just expand to the parent container, but not further. The height is ignored altogether.

Why, then, can’t I just omit the dimensions if they are irrelevant?

Well, picture yourself as the browser. You just got the HTML payload and started to parse it. The goal is to display content as fast as possible, so when you encounter the image tag, you start fetching the image from the URL.

Suppose the image’s dimensions have been omitted. Now you finished parsing and want to lay out the page. What are the dimensions of this image? You don’t know until the image is loaded, so you lay out the page with zeroed image dimensions.

When the image finally loads in, it causes a reflow, meaning you as the browser have to recalculate the entire page layout with the real image dimensions. This is suboptimal.

This is why width and height are crucial, especially for SEO-heavy websites and images that are instantly visible when the user navigates to that page. These dimensions tell the browser’s layout engine the expected aspect ratio of the image. Laying out the page, the engine then can calculate the displayed dimensions based off the width attribute and the aspect ratio.

Choosing the right format

Browsers support a handful of image formats (sorted by age, oldest first):

Which one should you choose?

One important distinction is to be made upfront: the list is exclusively covering raster image formats, except for SVG (vector format). You should always try to use SVG if you have it. Because SVG is (most of the time) pretty small, SVG images are consuming only a few kb at most.

And, because it is a vector format, it encodes shapes and colors instead of pixel values, meaning you can render it at arbitrary resolution. Due to that property, SVG images are not subject to the <img/> optimization strategies laid out in the rest of this article.

Focussing on raster image formats

The remaining formats store pixel data, decoding to RGB(A) values. TlDr; pick WebP or AVIF.

Choosing one of the newer formats has many advantages, such as fast/parallel decoding, progressive enhancement, better compression, more features, better-looking images at harder compression than other formats, etc. The potential downside is lacking support. But speaking realistically, most users have the latest modern browsers that support these.

WebP

Support among browsers for WebP is almost universal, with only Internet Explorer missing support. WebP supports lossless compression and lossy compression with variable levels. WebP is an excellent choice for lossy compression and, currently, the best choice for lossless compression.

AVIF

Based off the AV1 video standard, the newer AVIF format excels at decoding speed but only supports lossy compression. If you are crazing for the lastest and greatest with surprisingly good support (baseline 2024), choose it.

JPEG XL

This format was not listed because it is not even remotely ready for production. There has been a lot of controversy around this format, but in my opinion, it is the future. Read more here.


Now, with the right format in hand, we can continue to arguably the most important part of image optimization: sizing.

Sizing and quality

I would like to start off with an example.

Let’s say, you wanted to host an image for your website. You converted it from PNG or whatever to lossless WebP using some online converter. What dimensions would that be? Well, duh, the dimensions of the image you want to host… Let’s say 1920x1080 (Full HD).

I have done that exact process for a picture of some wood and ended up with a 2.25MB file. Now imagine 10,000 users going to your Website and having to download this 2.25MB file.

This would end up in 22.5GB of bandwidth for your server (bad!). This is also bad for the users that do not need a FHD image because they have fewer pixels on their phone screen, for example. Or the image is not even displayed as FHD, marking a lot of unused bytes downloaded. Or what about the users on a bad connection? Ain’t nobody want to download 2.25MB on cellular E.

This problem seems hard to solve because it heavily depends on the user client’s needs. Going extreme the other way, serving a low-res image results in bad image scaling and your website looks bad. Basically, we are yearning for a solution that lets the client pick what to download.

The solution

There are attributes on the <img/> tag that solve these problems. The core idea is that you (as the host) need to provide various versions of your image, each with a different resolution. And you need to provide the client with meta-information on what the image URLs are, their sizes, and when to pick what image. Using this information, the client itself figures out what to download.

The implementation and syntax of this information is rather unintuitive and the documentation is spare. Which is why we will look at another example, whose methods can be generalized.

Example: a basic blog

Picture this: you want to start a basic block and need to optimize the hero image of an article page. Let this be your layout for desktop:

and shrinking to

for mobile. How would you optimize the hero image? Let’s say that you have hero image sources at 1920x1080 which you want to use (the procedure is not limited to 16:9 aspect ratio images).

First, observe that your hero image does not get larger than 600 pixels wide, due to the max width on the container. At 1:1 pixel mapping (meaning that one pixel in HTML gets mapped to one physical pixel), the perfect image dimensions would be 600x337.5, rounding up to 600x338.

Note, that I have mentioned pixel mappings. This is important because not every device maps one HTML pixel to one physical one. On phones, for example, because they have such a high PPI (pixels per inch), websites would look tiny. This is why they “lie” to the browser about their screen dimensions but actually render the page to the physical dimensions. This also is relevant for users on desktops with scaling other than 100% and zoomed-in users.

Therefore, assuming a 1:1 pixel mapping is insufficient. This is why we should provide higher resolution images, e.g., at 1.5x, 2x, or even at 3x the HTML pixel size.

Now to the users that have smaller screens and less PPI. For them, we provide scaled-down variants at 0.75x, 0.5x, and 0.25x. Alternatively, you can provide images in various widths, instead of scaling of the base image (e.g., 400w, 300w, 200w, …).

Choosing quality

Most images, including hero images for blogs, presentation images, etc., do not need lossless encoding. The goal of the image is not to preserve all data, but to save time, bandwidth, and provide a good-ish looking visuals. Using WebP, a good quality recommendation is 80%.

Resulting Images

For our example, this means we need to host the following images:

Image path/nameWidth x HeightRaw Size (80% Q)
/your-image-base-path/image-name@0.25x.webp150x845KB
/your-image-base-path/image-name@0.5x.webp300x16914KB
/your-image-base-path/image-name@0.75x.webp450x25327KB
/your-image-base-path/image-name@1x.webp600x33841KB
/your-image-base-path/image-name@1.5x.webp900x50674KB
/your-image-base-path/image-name@2x.webp1200x675106KB
/your-image-base-path/image-name@3x.webp1800x1013183KB

(The size was from my testing with the wood picture mentioned earlier.)

The sum of all images’ sizes is even smaller than just the source image’s size (which was 2.25MB).

Additional notes

For such small images like 150x84 or 300x169, sometimes it is not worth hosting them because they only occupy a few KB.

What about the HTML?

Continuing the example from before, now let’s actually tell the client how to select and fetch these images. For that we need the srcset and sizes attribute of <img>.

The srcset attribute specifies the set of image source URLs we want the <img/> to have. It is a list of URLs, followed by <WIDTH>w. This is how we tell the client what images this <img/> could display.

The way the browser chooses the image is based off the sizes attribute: it is a list of media conditions, followed by the image width when the media condition is true.

Instead of discussing the exact syntax, look at the example hero image:

<img
    srcset="
        /your-image-base-path/image-name@0.25x.webp 150w,
        /your-image-base-path/image-name@0.5x.webp 300w,
        /your-image-base-path/image-name@0.75x.webp 450w,
        /your-image-base-path/image-name@1x.webp 600w,
        /your-image-base-path/image-name@1.5x.webp 900w,
        /your-image-base-path/image-name@2x.webp 1200w,
        /your-image-base-path/image-name@3x.webp 1800w
    "
    sizes="(max-width: 600px) 100vw, 600px"
    width="1920"
    height="1080"
    alt="Logs of wood."
>
Logs of wood.

(You can go into the Devtools → Network Manager and inspect how resizing the window and zooming triggers the browser to fetch other images in the srcset.)

The sizes attribute matches the behavior of the image resizing at breakpoints. When the viewport has a maximum width of 600px, the image is expected to cover ~100% of the viewport’s width, which is what 100vw (vw = “visual width”) encodes.

When the viewport is larger than 600px, the hero image in our example stays at 600px. Meaning the first media condition (max-width: 600px) fails and the browser tries to match the next media condition, but there is none, so it assumes a width of 600px (as specified).

Now that it has the expected image width, it chooses what image to download based off the specified image widths in the srcset attribute. And remember the width and height attributes so that the browser’s layout engine can predict the element size. Because the image aspect ratio does not change for the various image resolutions, you can just plug in the source image dimensions, but this is up to you.

Fixed-Size Images Shorthand Syntax

If you have fixed-size images that always will occupy the same size in the HTML, you can use this shorthand srcset syntax:

<img
    srcset="
        /image@1x.webp,
        /image@1.5x.webp 1.5x,
        /image@2x.webp 2x,
        ...
    "
    alt="..."
    width="..."
    height="..."
>

Advanced Features

You can use other media conditions/features for more fine-grained control over the images the client requests. A full list is here. For example, you could fetch different images based off the user’s color gamut, providing sRGB and P3 versions.

Cache control

Ideally, images (or any static resources even) should not be re-fetched when encountered multiple times because they did not change. This is what caching does. When the browser first fetches your images, the server’s HTTP(s) response may (or should) include the Cache-Control header specifying how long the resource should be cached.

This is a thing your server may do, so if you want caching: inspect your server. But how long should you cache the images? Because maybe your image path includes the slug of the article, so when you want to change the image, the URL does not change in response. The client will think that the resource content has not changed and read the cache.

Hashing Resources

The solution to this problem is, surprisingly, to hash the file contents (like the image) and include parts of the hash in the URL. This is what Astro does when optimizing images with the <Image/> component and Vite does when bundling JS or other resources.

This way, the server can send a cache control header to the client, specifying one month of caching, and whenever the file changes, it is a new URL for the client, so it does not mistakenly read the cache.

Framework integration and tools

I, personally, have used a custom Rust script using tokio and image to mass-optimize images. I’ve also used GIMP for manual optimization.

For this blog I use Astro, which exposes the <Image/> component that does all the stuff mentioned in this article automatically.

Progressive enhancement and the future of image optimization

In an ideal world, we would not have to think about all this. It would be cool if the client could just load as many bytes as it needs without choosing between source files.

This is what JPEG XL promises. Even if the file is 1MB the client can progressively load images rendering early responses as blurry. The JPEG XL community website has great images covering this (you’ll have to scroll a bit).

Art Direction

“Art Direction” is a somewhat similar problem which the picture element solves. It involves displaying different images (not just different resolutions) for different viewport sizes. Maybe I’ll cover this in the future, the following sources do tho.

Other excellent information sources

Here (someday) there will be recommendations