Story heading

10 ways to speed up web image loading

December 19, 2023

According to the HTTP Archive, images make up almost half of the weight of an average website, with around a megabyte per page. That is a megabyte that every user must download to see your website, which can often take a while for people on slow connections. Luckily, there are ways to mitigate this without much difficulty, from using modern image formats like AVIF or WebP, to using CDNs optimized for images, to replacing raster icons with SVGs. Here are 10 ways to improve image loading on a site.

If you want to learn more about how to optimize your website, be sure to check out: - 10 ways to speed up font loading - 10 ways to speed up JavaScript loading

How to make images load faster

1. Serve correctly sized images

Images do not need to be the same size for all devices. An image might look blurry on one display, like a 4k computer screen, but could still look very sharp on a smaller display, like a phone. It is a waste to serve higher definition images meant for those larger, higher resolution displays, to smaller displays. Luckily, the web provides mechanisms by default to help you serve different images based on screen resolution: the srcset attribute.

srcset allow you to pass multiple different sources that are only activated if a particular condition is true, like if the screen width is over 1024 pixels. You can also describe a pixel density rather than a fixed width. To implement srcset, all you have to do is this:

<img
	src="/images/default.webp"
	sizes="(max-width: 720px) 600px, 1200px"
	srcset="/images/small.png 600w, /images/default.png 1200w"
/>

Let’s go through this step by step. First, we define the default image URL, which can be used for browsers that do not support the sizes and srcset attribute. Next we define the image sizes using the sizes attribute. Here, we can pass a media query to select the 600px wide image only if the screen is below 720 pixels wide. Otherwise, browsers will use the second declaration, a 1200px image. However, the browser still does not know which URL to select.

In order to send the browser to the right image URL based on the width in sizes, we use srcset. In the above example, when the size from sizes is 600 pixels, we tell the browser to request /images/small.png, and if the image width is 1200px, the browser requests images.default.png. While this example only has two images, you can add as many as you want for any valid media query.

Browsers might not always choose images based on just screen width. Network connections and other factors can affect the browser’s choice.

2. Serve images with modern formats

When you think of the format an image is in, you probably think of PNGs or JPEGs. These are the most common images formats, so that is for good reason. Unfortunately, they use somewhat dated compression methods, and many modern image formats can help you compress your images better without sacrificing quality. The two most notable of these are WebP and AVIF.

WebP, a format developed by Google in 2010 based on the VP8, offers superior compression compared to most other image formats while having 96%+ browser support. AVIF is another image format, developed by the Alliance for Open Media as a derivative of AV1, that offers compression even better than WebP. Unfortunately, AV1 is currently unsupported in older browsers and Microsoft Edge due to “licensing” issues.

Here is an example of an AV1 image versus its JPG equivalent (note this will not work on Edge due to the above issue):

AVIF compressed image of Singapore; 70% smallerUncompressed image of singapore

Photo by Mike Enerio on Unsplash

The AVIF image is 50-70% smaller. Note that the size difference can vary but should be in that area. There is almost no quality difference between them, and in certain cases, the AVIF image seems to have removed some noise present in the JPG.

It is also pretty simple to use new formats. You can use an app like Squoosh to easily convert images to different formats. From there, you can use the new images like how you would use other images.

However, you might want to offer the most modern image format to newer browsers while supporting older formats for browsers that don’t support newer formats. Luckily, this isn’t too hard to implement using the <picture> element. <picture> works similarly to srcset and sizes, but it is a separate HTML element, and supports format fallbacks as well as media queries. You can implement <picture>s like this:

<picture>
	<source srcset="/images/avif-optimized.avif" type="image/avif" />
	<!-- First choice for browser --->
	<source srcset="/images/webp-optimized.webp" type="image/webp" />
	<!-- Used by browser if first choice is not supported --->
	<img src="/images/fallback.jpg" alt="photo" width="1200" height="600" />
	<!-- Used by browser if both previous choices are not supported; other attributes, like `alt` and `sizes`, will be shared across all possible images --->
</picture>

You can also use srcset as you would with individual <img> elements, except that sizes must be specified on the final img element rather than a source.

3. Use an image-optimized CDN

In the previous two tips, we have shown how to optimize images manually. Manually optimization works well in many cases, but sometimes, you don’t want to deal with all of that. You might also want to be able to provide a wider variety of optimizations without having to store countless image variants. There are image optimized CDNs available that will perform a lot of these transformations automatically. For example, ByteofDev (this blog) uses ImageKit, which, given device information given in the image request, will provide an image optimized to use the best format that device supports, with the size optimized for the device’s screen size. All of this is done on the server, meaning you only need one <img> element with one source.

Of course, there are some disadvantages to image-optimized CDNs. They can cost quite a bit, especially compared to a standard CDN’s incredibly low costs. They also introduce more complexity because they usually act as a proxy between whatever storage solution you use and the user. Of course, that isn’t unlike how other CDNs work. Perhaps I have been spoiled by hosting services that integrate CDNs into the host :). However, certain services offer integrated storage.

Here is a list of image-optimized CDNs that you might want to check out:

  • ImageKit - Less advanced optimizations; includes extra features like integrated storage
  • Cloudflare Images - Great for if you are already using Cloudflare; includes integrated storage; optimized for storing user generated content
  • Cloudinary - Likely the most popular image-optimized CDN; very large feature set; can get expensive
  • Fastly - Good for existing Fastly users; Slightly smaller feature set than some competitors;

4. Lazy load images

Many images on your website might not be seen by the user upon page load, but by default, all images load immediately. The “eager” nature of these images causes users to download resources that might be halfway down the page. The solution to this is to use lazy loading. By default, HTML images load eagerly, meaning they are requested immediately as the page loads, no matter where on the page they are. Luckily, by adding the loading="lazy" attribute, you can make it so the browser only loads images when the user scrolls near them.

<img src="/images/lazy.webp" loading="lazy" />
<!-- Lazy loaded! -->

Lazy loading is supported by 87% of users, including all modern browsers. You can also implement lazy loading through JavaScript, but at this point, it isn’t really worth the complexity and performance cost.

Do not lazy load images you expect to be in the user’s initial viewport because this will delay their loading unnecessarily.

5. Decode images asynchronously

In a similar vein to image lazy loading, you can also enable asynchronous image decoding. In Chrome and Safari, images are decoded synchronously by default. Synchronous decoding allows for all content to be displayed simultaneously but comes at the cost of speed, as the user needs to wait for the image to be decoded before content can be displayed. To solve this, you can use decoding="asynchronous" which will not make the browser wait for the image to decode.

<img src="/images/lazy.webp" loading="lazy" decoding="asynchronous" />
<!-- Lazy loaded and asynchronously decoded! -->

Just like lazy loading, you should use asynchronous decoding with care. Test the results before implementing this widely, as it can cause flashes of unstyled content (FOUC).

6. Provide low resolution placeholders

In certain cases, it can be helpful to provide blurry/low resolution placeholders for images. Placeholders reduce perceived loading time by showing a blurry version of the image before the image finishes loading. You can implement this using tools like BlurHash, which turns an image into a short Base83 binary string that can be decoded into a blurry placeholder before the full image loads. Many image CDNs support this as well. If none of these make sense and you are willing to sacrifice some looks, you can use a low-resolution placeholder.

Unfortunately, all of these options require at least some JavaScript to switch out images when the full-resolution version loads. The only way to implement placeholders without JavaScript is to use a little-known feature of JPEGs called progressive loading, where, rather than loading the image top-down, pixels in the image are distributed in such a way as to start with a low-definition version of the whole picture. Unfortunately, this means you cannot use other image formats like AVIF or WebP, as those do not support progressive loading. In the future, JPEG-XL, a new JPEG replacement with compression comparable to AVIF, might allow you to implement this without using JPEG. JPEG-XL is not supported in any browsers other than Safari at the moment, but that might change in the future.

7. Cache Images

Caching is one of the best ways to improve performance for any asset. If the server indicates caching is allowed, browsers will store assets locally, meaning the user doesn’t have to request them again when they next visit the site. Caching comes with the risk of an update to an image not being shown to the user, as their cached version does not have this update. However, you can mitigate this by changing the filename of the asset when the content changes, similar to what bundlers do. To use caching, you need to add an HTTP Cache-Control header to your HTTP responses, which can be done in varying ways depending on what you are using as a CDN. The cache control header should look something like this:

Cache-Control: max-age=86400

In the above example, browsers will cache the result for one day (86,400 seconds). If you know your content will not change without the filename/path changing, you can make this a lot longer.

As previously mentioned, implementing this is very dependent on your hosting provider/server implementation/CDN. I recommend looking up documentation on how to set HTTP headers. Many CDNs, like Cloudflare, also provide specific settings for cache control.

8. Define image dimensions

While defining image dimensions doesn’t necessarily speed up image loading itself, it can improve the user experience while the image is loading by reducing Layout Shift. If you do not define dimensions, the browser will not know the size of the image until it loads, and when it does load, it will shift all content below the image down, making content harder to read. If you define the image dimensions, the browser will reserve space for that image before it loads, preventing layout shift. There are two ways to allow the browser to reserve space: explicit attributes and CSS.

The simplest way to allow the browser to reserve is to explicitly define width and height on the <img> element.

<img src="/images/default.webp" alt="photo" width="1200" height="600" />

That is the best option if you know the exact dimensions and don’t expect them to change based on screen size or other factors. However, sometimes, you might need a little more flexibility. You can get this flexibility from CSS aspect-ratio. `aspect-ratio allows you to define an aspect ratio, which combined with one dimension, can help the browser accurately reserve space for the image.

<img src="images/default.webp" alt="photo" />
<style>
	img {
		width: 100vw;
		aspect-ratio: 16 / 9;
	}
</style>

The above CSS tells the browser to make the image fill the screen’s width and for the height to be 9/16 of whatever the width is. You can change the aspect ratio if the image isn’t 16:9, and even use height as the basis for the width instead.

9. Layout images lazily with content-visibility

By default, images are rendered in the layout upon page load. If you have a lot of images, this can use up a lot of unnecessary compute and slow down overall rendering. Luckily, modern browsers provide a property can control rendering: content-visibility. Setting content-visibility to auto tells the browser to only render the image when the user scrolls near it, rather than rendering everything upon page load.

<img src="images/default.webp" />
<style>
	img {
		content-visibility: auto;
	}
</style>

Once again, apply content-visility with care. Avoid using it for images viewable in the initial viewport.

10. Use SVGs when possible

This is perhaps the most significant tip I have, as SVGs are incredibly underrated. Many raster images can be replaced with SVGs, leading to significantly smaller file sizes and sharper images.

SVG (scalable vector graphic) is unlike the other image formats we discussed previously. Where most image formats are raster (also commonly referred to as bitmap), meaning they define what are essentially grids of pixels, SVG is a vector graphics format, meaning it defines images in terms of shapes and lines. SVGs tend to scale from complexity, not resolution. This makes an image scalable to any resolution, as computers can render the figures at a very high definition without the image defining each pixel. SVGs are also more readable because they use XML rather than a binary format. In fact, many designers right SVGs directly in text.

SVGs are most popular for small images like icons, but you can use them in pretty much any image that has simple objects that were created digitally. You should never vectorize photos, as even though there are automatic vectorizers, photos have noise and complex lighting that are unsuitable for being vectorized. The other exception is images that are very complex or use other images that are only available rasterized. Other than that, SVGs can work quite well. For example, many of the heading photos in this blog are SVGs, allowing me to deliver sharp images with <10kb payloads.

Implementing SVGs in a website is very similar to implementing raster images. To create an SVG file, use a design program like Inkscape or Adobe Illustrator and export as an SVG. Make sure everything you use within your image is an SVG, or else you will lose the benefits of an SVG. Once you do this, you can use the SVG in an <img> tag.

<img src="/images/default.svg" />

You can also embed the SVG directly into HTML, meaning you don’t need to send another request for the SVG.

<svg>
	<!-- SVG here -->
</svg>

However, embed with care, as it makes the SVG harder to cache and means the SVG blocks page loading. Inlining SVGs, just like any other assets, should only be done for small SVGs.

Conclusion

Thanks for reading! If you have any tips you think I should add, don’t hesitate to get in touch with me (I should eventually have a comment section set up :)). I hope this helps you speed up your website images; if you want to speed up other areas of your website, like fonts or JavaScript, check out these articles:

Share

Sign up for email updates

Get interesting posts about web development and more programming straight in your inbox!